mirror of
https://github.com/reactos/syzkaller.git
synced 2024-11-24 11:59:58 +00:00
39449875b6
.config is linux-ism. We have a convention that kernel config is copied to kernel.config file. Use it.
374 lines
10 KiB
Go
374 lines
10 KiB
Go
// Copyright 2018 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 bisect
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/google/syzkaller/pkg/build"
|
|
"github.com/google/syzkaller/pkg/instance"
|
|
"github.com/google/syzkaller/pkg/mgrconfig"
|
|
"github.com/google/syzkaller/pkg/osutil"
|
|
"github.com/google/syzkaller/pkg/report"
|
|
"github.com/google/syzkaller/pkg/vcs"
|
|
)
|
|
|
|
type Config struct {
|
|
Trace io.Writer
|
|
Fix bool
|
|
BinDir string
|
|
DebugDir string
|
|
Kernel KernelConfig
|
|
Syzkaller SyzkallerConfig
|
|
Repro ReproConfig
|
|
Manager mgrconfig.Config
|
|
}
|
|
|
|
type KernelConfig struct {
|
|
Repo string
|
|
Branch string
|
|
Commit string
|
|
Cmdline string
|
|
Sysctl string
|
|
Config []byte
|
|
Userspace string
|
|
}
|
|
|
|
type SyzkallerConfig struct {
|
|
Repo string
|
|
Commit string
|
|
Descriptions string
|
|
}
|
|
|
|
type ReproConfig struct {
|
|
Opts []byte
|
|
Syz []byte
|
|
C []byte
|
|
}
|
|
|
|
type env struct {
|
|
cfg *Config
|
|
repo vcs.Repo
|
|
bisecter vcs.Bisecter
|
|
head *vcs.Commit
|
|
inst *instance.Env
|
|
numTests int
|
|
buildTime time.Duration
|
|
testTime time.Duration
|
|
}
|
|
|
|
const NumTests = 10 // number of tests we do per commit
|
|
|
|
// Run does the bisection and returns:
|
|
// - if bisection is conclusive, the single cause/fix commit
|
|
// - for cause bisection report is the crash on the cause commit
|
|
// - for fix bisection report is nil
|
|
// - if bisection is inconclusive, range of potential cause/fix commits
|
|
// - report is nil in such case
|
|
// - if the crash still happens on the oldest release/HEAD (for cause/fix bisection correspondingly),
|
|
// no commits and the crash report on the oldest release/HEAD
|
|
// - if the crash is not reproduced on the start commit, an error
|
|
func Run(cfg *Config) ([]*vcs.Commit, *report.Report, error) {
|
|
if err := checkConfig(cfg); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
cfg.Manager.Cover = false // it's not supported somewhere back in time
|
|
repo, err := vcs.NewRepo(cfg.Manager.TargetOS, cfg.Manager.Type, cfg.Manager.KernelSrc)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
bisecter, ok := repo.(vcs.Bisecter)
|
|
if !ok {
|
|
return nil, nil, fmt.Errorf("bisection is not implemented for %v", cfg.Manager.TargetOS)
|
|
}
|
|
env := &env{
|
|
cfg: cfg,
|
|
repo: repo,
|
|
bisecter: bisecter,
|
|
}
|
|
if cfg.Fix {
|
|
env.log("bisecting fixing commit since %v", cfg.Kernel.Commit)
|
|
} else {
|
|
env.log("bisecting cause commit starting from %v", cfg.Kernel.Commit)
|
|
}
|
|
start := time.Now()
|
|
commits, rep, err := env.bisect()
|
|
env.log("revisions tested: %v, total time: %v (build: %v, test: %v)",
|
|
env.numTests, time.Since(start), env.buildTime, env.testTime)
|
|
if err != nil {
|
|
env.log("error: %v", err)
|
|
return nil, nil, err
|
|
}
|
|
if len(commits) == 0 {
|
|
if cfg.Fix {
|
|
env.log("the crash still happens on HEAD")
|
|
} else {
|
|
env.log("the crash already happened on the oldest tested release")
|
|
}
|
|
env.log("crash: %v\n%s", rep.Title, rep.Report)
|
|
return nil, rep, nil
|
|
}
|
|
what := "bad"
|
|
if cfg.Fix {
|
|
what = "good"
|
|
}
|
|
if len(commits) > 1 {
|
|
env.log("bisection is inconclusive, the first %v commit could be any of:", what)
|
|
for _, com := range commits {
|
|
env.log("%v", com.Hash)
|
|
}
|
|
return commits, nil, nil
|
|
}
|
|
com := commits[0]
|
|
env.log("first %v commit: %v %v", what, com.Hash, com.Title)
|
|
env.log("cc: %q", com.CC)
|
|
if rep != nil {
|
|
env.log("crash: %v\n%s", rep.Title, rep.Report)
|
|
}
|
|
return commits, rep, nil
|
|
}
|
|
|
|
func (env *env) bisect() ([]*vcs.Commit, *report.Report, error) {
|
|
cfg := env.cfg
|
|
var err error
|
|
if env.inst, err = instance.NewEnv(&cfg.Manager); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if env.head, err = env.repo.CheckoutBranch(cfg.Kernel.Repo, cfg.Kernel.Branch); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if err := build.Clean(cfg.Manager.TargetOS, cfg.Manager.TargetVMArch,
|
|
cfg.Manager.Type, cfg.Manager.KernelSrc); err != nil {
|
|
return nil, nil, fmt.Errorf("kernel clean failed: %v", err)
|
|
}
|
|
env.log("building syzkaller on %v", cfg.Syzkaller.Commit)
|
|
if err := env.inst.BuildSyzkaller(cfg.Syzkaller.Repo, cfg.Syzkaller.Commit); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if _, err := env.repo.CheckoutCommit(cfg.Kernel.Repo, cfg.Kernel.Commit); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
res, _, rep0, err := env.test()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
} else if res != vcs.BisectBad {
|
|
return nil, nil, fmt.Errorf("the crash wasn't reproduced on the original commit")
|
|
}
|
|
bad, good, rep1, err := env.commitRange()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if good == "" {
|
|
return nil, rep1, nil // still not fixed/happens on the oldest release
|
|
}
|
|
reports := make(map[string]*report.Report)
|
|
reports[cfg.Kernel.Commit] = rep0
|
|
commits, err := env.bisecter.Bisect(bad, good, cfg.Trace, func() (vcs.BisectResult, error) {
|
|
res, com, rep, err := env.test()
|
|
reports[com.Hash] = rep
|
|
if cfg.Fix {
|
|
if res == vcs.BisectBad {
|
|
res = vcs.BisectGood
|
|
} else if res == vcs.BisectGood {
|
|
res = vcs.BisectBad
|
|
}
|
|
}
|
|
return res, err
|
|
})
|
|
var rep *report.Report
|
|
if len(commits) == 1 {
|
|
rep = reports[commits[0].Hash]
|
|
}
|
|
return commits, rep, err
|
|
}
|
|
|
|
func (env *env) commitRange() (string, string, *report.Report, error) {
|
|
if env.cfg.Fix {
|
|
return env.commitRangeForFix()
|
|
}
|
|
return env.commitRangeForBug()
|
|
}
|
|
|
|
func (env *env) commitRangeForFix() (string, string, *report.Report, error) {
|
|
env.log("testing current HEAD %v", env.head.Hash)
|
|
if _, err := env.repo.SwitchCommit(env.head.Hash); err != nil {
|
|
return "", "", nil, err
|
|
}
|
|
res, _, rep, err := env.test()
|
|
if err != nil {
|
|
return "", "", nil, err
|
|
}
|
|
if res != vcs.BisectGood {
|
|
return "", "", rep, nil
|
|
}
|
|
return env.head.Hash, env.cfg.Kernel.Commit, nil, nil
|
|
}
|
|
|
|
func (env *env) commitRangeForBug() (string, string, *report.Report, error) {
|
|
cfg := env.cfg
|
|
tags, err := env.bisecter.PreviousReleaseTags(cfg.Kernel.Commit)
|
|
if err != nil {
|
|
return "", "", nil, err
|
|
}
|
|
if len(tags) == 0 {
|
|
return "", "", nil, fmt.Errorf("no release tags before this commit")
|
|
}
|
|
lastBad := cfg.Kernel.Commit
|
|
var lastRep *report.Report
|
|
for _, tag := range tags {
|
|
env.log("testing release %v", tag)
|
|
if _, err := env.repo.SwitchCommit(tag); err != nil {
|
|
return "", "", nil, err
|
|
}
|
|
res, _, rep, err := env.test()
|
|
if err != nil {
|
|
return "", "", nil, err
|
|
}
|
|
if res == vcs.BisectGood {
|
|
return lastBad, tag, nil, nil
|
|
}
|
|
if res == vcs.BisectBad {
|
|
lastBad = tag
|
|
lastRep = rep
|
|
}
|
|
}
|
|
return "", "", lastRep, nil
|
|
}
|
|
|
|
func (env *env) test() (vcs.BisectResult, *vcs.Commit, *report.Report, error) {
|
|
cfg := env.cfg
|
|
env.numTests++
|
|
current, err := env.repo.HeadCommit()
|
|
if err != nil {
|
|
return 0, nil, nil, err
|
|
}
|
|
bisectEnv, err := env.bisecter.EnvForCommit(current.Hash, cfg.Kernel.Config)
|
|
if err != nil {
|
|
return 0, nil, nil, err
|
|
}
|
|
compiler := filepath.Join(cfg.BinDir, bisectEnv.Compiler, "bin", "gcc")
|
|
compilerID, err := build.CompilerIdentity(compiler)
|
|
if err != nil {
|
|
return 0, nil, nil, err
|
|
}
|
|
env.log("testing commit %v with %v", current.Hash, compilerID)
|
|
buildStart := time.Now()
|
|
if err := build.Clean(cfg.Manager.TargetOS, cfg.Manager.TargetVMArch,
|
|
cfg.Manager.Type, cfg.Manager.KernelSrc); err != nil {
|
|
return 0, nil, nil, fmt.Errorf("kernel clean failed: %v", err)
|
|
}
|
|
_, err = env.inst.BuildKernel(compiler, cfg.Kernel.Userspace,
|
|
cfg.Kernel.Cmdline, cfg.Kernel.Sysctl, bisectEnv.KernelConfig)
|
|
env.buildTime += time.Since(buildStart)
|
|
if err != nil {
|
|
if verr, ok := err.(*osutil.VerboseError); ok {
|
|
env.log("%v", verr.Title)
|
|
env.saveDebugFile(current.Hash, 0, verr.Output)
|
|
} else if verr, ok := err.(build.KernelBuildError); ok {
|
|
env.log("%v", verr.Title)
|
|
env.saveDebugFile(current.Hash, 0, verr.Output)
|
|
} else {
|
|
env.log("%v", err)
|
|
}
|
|
return vcs.BisectSkip, current, nil, nil
|
|
}
|
|
testStart := time.Now()
|
|
results, err := env.inst.Test(NumTests, cfg.Repro.Syz, cfg.Repro.Opts, cfg.Repro.C)
|
|
env.testTime += time.Since(testStart)
|
|
if err != nil {
|
|
env.log("failed: %v", err)
|
|
return vcs.BisectSkip, current, nil, nil
|
|
}
|
|
bad, good, rep := env.processResults(current, results)
|
|
res := vcs.BisectSkip
|
|
if bad != 0 {
|
|
res = vcs.BisectBad
|
|
} else if NumTests-good-bad > NumTests/3*2 {
|
|
// More than 2/3 of instances failed with infrastructure error,
|
|
// can't reliably tell that the commit is good.
|
|
res = vcs.BisectSkip
|
|
} else if good != 0 {
|
|
res = vcs.BisectGood
|
|
}
|
|
return res, current, rep, nil
|
|
}
|
|
|
|
func (env *env) processResults(current *vcs.Commit, results []error) (bad, good int, rep *report.Report) {
|
|
var verdicts []string
|
|
for i, res := range results {
|
|
if res == nil {
|
|
good++
|
|
verdicts = append(verdicts, "OK")
|
|
continue
|
|
}
|
|
switch err := res.(type) {
|
|
case *instance.TestError:
|
|
if err.Boot {
|
|
verdicts = append(verdicts, fmt.Sprintf("boot failed: %v", err))
|
|
} else {
|
|
verdicts = append(verdicts, fmt.Sprintf("basic kernel testing failed: %v", err))
|
|
}
|
|
output := err.Output
|
|
if err.Report != nil {
|
|
output = err.Report.Output
|
|
}
|
|
env.saveDebugFile(current.Hash, i, output)
|
|
case *instance.CrashError:
|
|
bad++
|
|
rep = err.Report
|
|
verdicts = append(verdicts, fmt.Sprintf("crashed: %v", err))
|
|
output := err.Report.Report
|
|
if len(output) == 0 {
|
|
output = err.Report.Output
|
|
}
|
|
env.saveDebugFile(current.Hash, i, output)
|
|
default:
|
|
verdicts = append(verdicts, fmt.Sprintf("failed: %v", err))
|
|
}
|
|
}
|
|
unique := make(map[string]bool)
|
|
for _, verdict := range verdicts {
|
|
unique[verdict] = true
|
|
}
|
|
if len(unique) == 1 {
|
|
env.log("all runs: %v", verdicts[0])
|
|
} else {
|
|
for i, verdict := range verdicts {
|
|
env.log("run #%v: %v", i, verdict)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (env *env) saveDebugFile(hash string, idx int, data []byte) {
|
|
if env.cfg.DebugDir == "" || len(data) == 0 {
|
|
return
|
|
}
|
|
osutil.MkdirAll(env.cfg.DebugDir)
|
|
osutil.WriteFile(filepath.Join(env.cfg.DebugDir, fmt.Sprintf("%v.%v", hash, idx)), data)
|
|
}
|
|
|
|
func checkConfig(cfg *Config) error {
|
|
if !osutil.IsExist(cfg.BinDir) {
|
|
return fmt.Errorf("bin dir %v does not exist", cfg.BinDir)
|
|
}
|
|
if cfg.Kernel.Userspace != "" && !osutil.IsExist(cfg.Kernel.Userspace) {
|
|
return fmt.Errorf("userspace dir %v does not exist", cfg.Kernel.Userspace)
|
|
}
|
|
if cfg.Kernel.Sysctl != "" && !osutil.IsExist(cfg.Kernel.Sysctl) {
|
|
return fmt.Errorf("sysctl file %v does not exist", cfg.Kernel.Sysctl)
|
|
}
|
|
if cfg.Kernel.Cmdline != "" && !osutil.IsExist(cfg.Kernel.Cmdline) {
|
|
return fmt.Errorf("cmdline file %v does not exist", cfg.Kernel.Cmdline)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (env *env) log(msg string, args ...interface{}) {
|
|
fmt.Fprintf(env.cfg.Trace, msg+"\n", args...)
|
|
}
|