Golang程序  |  346行  |  8.27 KB

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