// 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/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
|
head *vcs.Commit
|
inst *instance.Env
|
numTests int
|
buildTime time.Duration
|
testTime time.Duration
|
}
|
|
type buildEnv struct {
|
compiler string
|
}
|
|
func Run(cfg *Config) (*vcs.Commit, error) {
|
repo, err := vcs.NewRepo(cfg.Manager.TargetOS, cfg.Manager.Type, cfg.Manager.KernelSrc)
|
if err != nil {
|
return nil, err
|
}
|
env := &env{
|
cfg: cfg,
|
repo: repo,
|
}
|
if cfg.Fix {
|
env.log("searching for fixing commit since %v", cfg.Kernel.Commit)
|
} else {
|
env.log("searching for guilty commit starting from %v", cfg.Kernel.Commit)
|
}
|
start := time.Now()
|
res, 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, err
|
}
|
if res == nil {
|
env.log("the crash is still unfixed")
|
return nil, nil
|
}
|
what := "bad"
|
if cfg.Fix {
|
what = "good"
|
}
|
env.log("first %v commit: %v %v", what, res.Hash, res.Title)
|
env.log("cc: %q", res.CC)
|
return res, nil
|
}
|
|
func (env *env) bisect() (*vcs.Commit, error) {
|
cfg := env.cfg
|
var err error
|
if env.inst, err = instance.NewEnv(&cfg.Manager); err != nil {
|
return nil, err
|
}
|
if env.head, err = env.repo.Poll(cfg.Kernel.Repo, cfg.Kernel.Branch); err != nil {
|
return nil, err
|
}
|
if err := build.Clean(cfg.Manager.TargetOS, cfg.Manager.TargetVMArch,
|
cfg.Manager.Type, cfg.Manager.KernelSrc); err != nil {
|
return 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, err
|
}
|
if _, err := env.repo.SwitchCommit(cfg.Kernel.Commit); err != nil {
|
return nil, err
|
}
|
if res, err := env.test(); err != nil {
|
return nil, err
|
} else if res != vcs.BisectBad {
|
return nil, fmt.Errorf("the crash wasn't reproduced on the original commit")
|
}
|
res, bad, good, err := env.commitRange()
|
if err != nil {
|
return nil, err
|
}
|
if res != nil {
|
return res, nil // happens on the oldest release
|
}
|
if good == "" {
|
return nil, nil // still not fixed
|
}
|
return env.repo.Bisect(bad, good, cfg.Trace, func() (vcs.BisectResult, error) {
|
res, err := env.test()
|
if cfg.Fix {
|
if res == vcs.BisectBad {
|
res = vcs.BisectGood
|
} else if res == vcs.BisectGood {
|
res = vcs.BisectBad
|
}
|
}
|
return res, err
|
})
|
}
|
|
func (env *env) commitRange() (*vcs.Commit, string, string, error) {
|
if env.cfg.Fix {
|
return env.commitRangeForFix()
|
}
|
return env.commitRangeForBug()
|
}
|
|
func (env *env) commitRangeForFix() (*vcs.Commit, string, string, error) {
|
env.log("testing current HEAD %v", env.head.Hash)
|
if _, err := env.repo.SwitchCommit(env.head.Hash); err != nil {
|
return nil, "", "", err
|
}
|
res, err := env.test()
|
if err != nil {
|
return nil, "", "", err
|
}
|
if res != vcs.BisectGood {
|
return nil, "", "", nil
|
}
|
return nil, env.head.Hash, env.cfg.Kernel.Commit, nil
|
}
|
|
func (env *env) commitRangeForBug() (*vcs.Commit, string, string, error) {
|
cfg := env.cfg
|
tags, err := env.repo.PreviousReleaseTags(cfg.Kernel.Commit)
|
if err != nil {
|
return nil, "", "", err
|
}
|
for i, tag := range tags {
|
if tag == "v3.8" {
|
// v3.8 does not work with modern perl, and as we go further in history
|
// make stops to work, then binutils, glibc, etc. So we stop at v3.8.
|
// Up to that point we only need an ancient gcc.
|
tags = tags[:i]
|
break
|
}
|
}
|
if len(tags) == 0 {
|
return nil, "", "", fmt.Errorf("no release tags before this commit")
|
}
|
lastBad := cfg.Kernel.Commit
|
for i, tag := range tags {
|
env.log("testing release %v", tag)
|
commit, err := env.repo.SwitchCommit(tag)
|
if err != nil {
|
return nil, "", "", err
|
}
|
res, err := env.test()
|
if err != nil {
|
return nil, "", "", err
|
}
|
if res == vcs.BisectGood {
|
return nil, lastBad, tag, nil
|
}
|
if res == vcs.BisectBad {
|
lastBad = tag
|
}
|
if i == len(tags)-1 {
|
return commit, "", "", nil
|
}
|
}
|
panic("unreachable")
|
}
|
|
func (env *env) test() (vcs.BisectResult, error) {
|
cfg := env.cfg
|
env.numTests++
|
current, err := env.repo.HeadCommit()
|
if err != nil {
|
return 0, err
|
}
|
be, err := env.buildEnvForCommit(current.Hash)
|
if err != nil {
|
return 0, err
|
}
|
compilerID, err := build.CompilerIdentity(be.compiler)
|
if err != nil {
|
return 0, 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, fmt.Errorf("kernel clean failed: %v", err)
|
}
|
err = env.inst.BuildKernel(be.compiler, cfg.Kernel.Userspace,
|
cfg.Kernel.Cmdline, cfg.Kernel.Sysctl, cfg.Kernel.Config)
|
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 {
|
env.log("%v", err)
|
}
|
return vcs.BisectSkip, nil
|
}
|
testStart := time.Now()
|
results, err := env.inst.Test(8, 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, nil
|
}
|
bad, good := env.processResults(current, results)
|
res := vcs.BisectSkip
|
if bad != 0 {
|
res = vcs.BisectBad
|
} else if good != 0 {
|
res = vcs.BisectGood
|
}
|
return res, nil
|
}
|
|
func (env *env) processResults(current *vcs.Commit, results []error) (bad, good int) {
|
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++
|
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
|
}
|
|
// Note: linux-specific.
|
func (env *env) buildEnvForCommit(commit string) (*buildEnv, error) {
|
cfg := env.cfg
|
tags, err := env.repo.PreviousReleaseTags(commit)
|
if err != nil {
|
return nil, err
|
}
|
be := &buildEnv{
|
compiler: filepath.Join(cfg.BinDir, "gcc-"+linuxCompilerVersion(tags), "bin", "gcc"),
|
}
|
return be, nil
|
}
|
|
func linuxCompilerVersion(tags []string) string {
|
for _, tag := range tags {
|
switch tag {
|
case "v4.12":
|
return "8.1.0"
|
case "v4.11":
|
return "7.3.0"
|
case "v3.19":
|
return "5.5.0"
|
}
|
}
|
return "4.9.4"
|
}
|
|
func (env *env) saveDebugFile(hash string, idx int, data []byte) {
|
if env.cfg.DebugDir == "" || len(data) == 0 {
|
return
|
}
|
osutil.WriteFile(filepath.Join(env.cfg.DebugDir, fmt.Sprintf("%v.%v", hash, idx)), data)
|
}
|
|
func (env *env) log(msg string, args ...interface{}) {
|
fmt.Fprintf(env.cfg.Trace, msg+"\n", args...)
|
}
|