syzkaller/pkg/bisect/bisect.go
Dmitry Vyukov 39449875b6 syz-ci: don't assume kernel config is called .config
.config is linux-ism. We have a convention that kernel config
is copied to kernel.config file. Use it.
2019-05-12 11:38:18 +02:00

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...)
}