Home | History | Annotate | Download | only in syz-ci
      1 // Copyright 2017 syzkaller project authors. All rights reserved.
      2 // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
      3 
      4 package main
      5 
      6 import (
      7 	"fmt"
      8 	"io/ioutil"
      9 	"os"
     10 	"path/filepath"
     11 	"strings"
     12 	"syscall"
     13 	"time"
     14 
     15 	"github.com/google/syzkaller/pkg/log"
     16 	"github.com/google/syzkaller/pkg/mgrconfig"
     17 	"github.com/google/syzkaller/pkg/osutil"
     18 	"github.com/google/syzkaller/pkg/vcs"
     19 )
     20 
     21 const (
     22 	syzkallerRebuildPeriod = 12 * time.Hour
     23 	buildRetryPeriod       = 10 * time.Minute // used for both syzkaller and kernel
     24 )
     25 
     26 // SyzUpdater handles everything related to syzkaller updates.
     27 // As kernel builder, it maintains 2 builds:
     28 //  - latest: latest known good syzkaller build
     29 //  - current: currently used syzkaller build
     30 // Additionally it updates and restarts the current executable as necessary.
     31 // Current executable is always built on the same revision as the rest of syzkaller binaries.
     32 type SyzUpdater struct {
     33 	repo         vcs.Repo
     34 	exe          string
     35 	repoAddress  string
     36 	branch       string
     37 	descriptions string
     38 	gopathDir    string
     39 	syzkallerDir string
     40 	latestDir    string
     41 	currentDir   string
     42 	syzFiles     map[string]bool
     43 	targets      map[string]bool
     44 }
     45 
     46 func NewSyzUpdater(cfg *Config) *SyzUpdater {
     47 	wd, err := os.Getwd()
     48 	if err != nil {
     49 		log.Fatalf("failed to get wd: %v", err)
     50 	}
     51 	bin := os.Args[0]
     52 	if !filepath.IsAbs(bin) {
     53 		bin = filepath.Join(wd, bin)
     54 	}
     55 	bin = filepath.Clean(bin)
     56 	exe := filepath.Base(bin)
     57 	if wd != filepath.Dir(bin) {
     58 		log.Fatalf("%v executable must be in cwd (it will be overwritten on update)", exe)
     59 	}
     60 
     61 	gopath := filepath.Join(wd, "gopath")
     62 	os.Setenv("GOROOT", cfg.Goroot)
     63 	os.Unsetenv("GOPATH")
     64 	os.Setenv("PATH", filepath.Join(cfg.Goroot, "bin")+
     65 		string(filepath.ListSeparator)+os.Getenv("PATH"))
     66 	syzkallerDir := filepath.Join(gopath, "src", "github.com", "google", "syzkaller")
     67 	osutil.MkdirAll(syzkallerDir)
     68 
     69 	// List of required files in syzkaller build (contents of latest/current dirs).
     70 	files := map[string]bool{
     71 		"tag":             true, // contains syzkaller repo git hash
     72 		"bin/syz-ci":      true, // these are just copied from syzkaller dir
     73 		"bin/syz-manager": true,
     74 	}
     75 	targets := make(map[string]bool)
     76 	for _, mgr := range cfg.Managers {
     77 		mgrcfg, err := mgrconfig.LoadPartialData(mgr.ManagerConfig)
     78 		if err != nil {
     79 			log.Fatalf("failed to load manager %v config: %v", mgr.Name, err)
     80 		}
     81 		os, vmarch, arch := mgrcfg.TargetOS, mgrcfg.TargetVMArch, mgrcfg.TargetArch
     82 		targets[os+"/"+vmarch+"/"+arch] = true
     83 		files[fmt.Sprintf("bin/%v_%v/syz-fuzzer", os, vmarch)] = true
     84 		files[fmt.Sprintf("bin/%v_%v/syz-execprog", os, vmarch)] = true
     85 		files[fmt.Sprintf("bin/%v_%v/syz-executor", os, arch)] = true
     86 	}
     87 	syzFiles := make(map[string]bool)
     88 	for f := range files {
     89 		syzFiles[f] = true
     90 	}
     91 	return &SyzUpdater{
     92 		repo:         vcs.NewSyzkallerRepo(syzkallerDir),
     93 		exe:          exe,
     94 		repoAddress:  cfg.SyzkallerRepo,
     95 		branch:       cfg.SyzkallerBranch,
     96 		descriptions: cfg.SyzkallerDescriptions,
     97 		gopathDir:    gopath,
     98 		syzkallerDir: syzkallerDir,
     99 		latestDir:    filepath.Join("syzkaller", "latest"),
    100 		currentDir:   filepath.Join("syzkaller", "current"),
    101 		syzFiles:     syzFiles,
    102 		targets:      targets,
    103 	}
    104 }
    105 
    106 // UpdateOnStart does 3 things:
    107 //  - ensures that the current executable is fresh
    108 //  - ensures that we have a working syzkaller build in current
    109 func (upd *SyzUpdater) UpdateOnStart(shutdown chan struct{}) {
    110 	os.RemoveAll(upd.currentDir)
    111 	exeTag, exeMod := readTag(upd.exe + ".tag")
    112 	latestTag := upd.checkLatest()
    113 	if exeTag == latestTag && time.Since(exeMod) < time.Minute {
    114 		// Have a freash up-to-date build, probably just restarted.
    115 		log.Logf(0, "current executable is up-to-date (%v)", exeTag)
    116 		if err := osutil.LinkFiles(upd.latestDir, upd.currentDir, upd.syzFiles); err != nil {
    117 			log.Fatal(err)
    118 		}
    119 		return
    120 	}
    121 	if exeTag == "" {
    122 		log.Logf(0, "current executable is bootstrap")
    123 	} else {
    124 		log.Logf(0, "current executable is on %v", exeTag)
    125 		log.Logf(0, "latest syzkaller build is on %v", latestTag)
    126 	}
    127 
    128 	// No syzkaller build or executable is stale.
    129 	lastCommit := exeTag
    130 	for {
    131 		lastCommit = upd.pollAndBuild(lastCommit)
    132 		latestTag := upd.checkLatest()
    133 		if latestTag != "" {
    134 			// The build was successful or we had the latest build from previous runs.
    135 			// Either way, use the latest build.
    136 			log.Logf(0, "using syzkaller built on %v", latestTag)
    137 			if err := osutil.LinkFiles(upd.latestDir, upd.currentDir, upd.syzFiles); err != nil {
    138 				log.Fatal(err)
    139 			}
    140 			if exeTag != latestTag {
    141 				upd.UpdateAndRestart()
    142 			}
    143 			return
    144 		}
    145 
    146 		// No good build at all, try again later.
    147 		log.Logf(0, "retrying in %v", buildRetryPeriod)
    148 		select {
    149 		case <-time.After(buildRetryPeriod):
    150 		case <-shutdown:
    151 			os.Exit(0)
    152 		}
    153 	}
    154 }
    155 
    156 // WaitForUpdate polls and rebuilds syzkaller.
    157 // Returns when we have a new good build in latest.
    158 func (upd *SyzUpdater) WaitForUpdate() {
    159 	time.Sleep(syzkallerRebuildPeriod)
    160 	latestTag := upd.checkLatest()
    161 	lastCommit := latestTag
    162 	for {
    163 		lastCommit = upd.pollAndBuild(lastCommit)
    164 		if latestTag != upd.checkLatest() {
    165 			break
    166 		}
    167 		time.Sleep(buildRetryPeriod)
    168 	}
    169 	log.Logf(0, "syzkaller: update available, restarting")
    170 }
    171 
    172 // UpdateAndRestart updates and restarts the current executable.
    173 // Does not return.
    174 func (upd *SyzUpdater) UpdateAndRestart() {
    175 	log.Logf(0, "restarting executable for update")
    176 	latestBin := filepath.Join(upd.latestDir, "bin", upd.exe)
    177 	latestTag := filepath.Join(upd.latestDir, "tag")
    178 	if err := osutil.CopyFile(latestBin, upd.exe); err != nil {
    179 		log.Fatal(err)
    180 	}
    181 	if err := osutil.CopyFile(latestTag, upd.exe+".tag"); err != nil {
    182 		log.Fatal(err)
    183 	}
    184 	if err := syscall.Exec(upd.exe, os.Args, os.Environ()); err != nil {
    185 		log.Fatal(err)
    186 	}
    187 	log.Fatalf("not reachable")
    188 }
    189 
    190 func (upd *SyzUpdater) pollAndBuild(lastCommit string) string {
    191 	commit, err := upd.repo.Poll(upd.repoAddress, upd.branch)
    192 	if err != nil {
    193 		log.Logf(0, "syzkaller: failed to poll: %v", err)
    194 		return lastCommit
    195 	}
    196 	log.Logf(0, "syzkaller: poll: %v (%v)", commit.Hash, commit.Title)
    197 	if lastCommit != commit.Hash {
    198 		log.Logf(0, "syzkaller: building ...")
    199 		lastCommit = commit.Hash
    200 		if err := upd.build(commit); err != nil {
    201 			log.Logf(0, "syzkaller: %v", err)
    202 		}
    203 	}
    204 	return lastCommit
    205 }
    206 
    207 func (upd *SyzUpdater) build(commit *vcs.Commit) error {
    208 	if upd.descriptions != "" {
    209 		files, err := ioutil.ReadDir(upd.descriptions)
    210 		if err != nil {
    211 			return fmt.Errorf("failed to read descriptions dir: %v", err)
    212 		}
    213 		for _, f := range files {
    214 			src := filepath.Join(upd.descriptions, f.Name())
    215 			dst := filepath.Join(upd.syzkallerDir, "sys", "linux", f.Name())
    216 			if err := osutil.CopyFile(src, dst); err != nil {
    217 				return err
    218 			}
    219 		}
    220 	}
    221 	cmd := osutil.Command("make", "generate")
    222 	cmd.Dir = upd.syzkallerDir
    223 	cmd.Env = append([]string{"GOPATH=" + upd.gopathDir}, os.Environ()...)
    224 	if _, err := osutil.Run(time.Hour, cmd); err != nil {
    225 		return fmt.Errorf("build failed: %v", err)
    226 	}
    227 	cmd = osutil.Command("make", "host", "ci")
    228 	cmd.Dir = upd.syzkallerDir
    229 	cmd.Env = append([]string{"GOPATH=" + upd.gopathDir}, os.Environ()...)
    230 	if _, err := osutil.Run(time.Hour, cmd); err != nil {
    231 		return fmt.Errorf("build failed: %v", err)
    232 	}
    233 	for target := range upd.targets {
    234 		parts := strings.Split(target, "/")
    235 		cmd = osutil.Command("make", "target")
    236 		cmd.Dir = upd.syzkallerDir
    237 		cmd.Env = append([]string{}, os.Environ()...)
    238 		cmd.Env = append(cmd.Env,
    239 			"GOPATH="+upd.gopathDir,
    240 			"TARGETOS="+parts[0],
    241 			"TARGETVMARCH="+parts[1],
    242 			"TARGETARCH="+parts[2],
    243 		)
    244 		if _, err := osutil.Run(time.Hour, cmd); err != nil {
    245 			return fmt.Errorf("build failed: %v", err)
    246 		}
    247 	}
    248 	cmd = osutil.Command("go", "test", "-short", "./...")
    249 	cmd.Dir = upd.syzkallerDir
    250 	cmd.Env = append([]string{"GOPATH=" + upd.gopathDir}, os.Environ()...)
    251 	if _, err := osutil.Run(time.Hour, cmd); err != nil {
    252 		return fmt.Errorf("tests failed: %v", err)
    253 	}
    254 	tagFile := filepath.Join(upd.syzkallerDir, "tag")
    255 	if err := osutil.WriteFile(tagFile, []byte(commit.Hash)); err != nil {
    256 		return fmt.Errorf("filed to write tag file: %v", err)
    257 	}
    258 	if err := osutil.CopyFiles(upd.syzkallerDir, upd.latestDir, upd.syzFiles); err != nil {
    259 		return fmt.Errorf("filed to copy syzkaller: %v", err)
    260 	}
    261 	return nil
    262 }
    263 
    264 // checkLatest returns tag of the latest build,
    265 // or an empty string if latest build is missing/broken.
    266 func (upd *SyzUpdater) checkLatest() string {
    267 	if !osutil.FilesExist(upd.latestDir, upd.syzFiles) {
    268 		return ""
    269 	}
    270 	tag, _ := readTag(filepath.Join(upd.latestDir, "tag"))
    271 	return tag
    272 }
    273 
    274 func readTag(file string) (tag string, mod time.Time) {
    275 	data, _ := ioutil.ReadFile(file)
    276 	tag = string(data)
    277 	if st, err := os.Stat(file); err == nil {
    278 		mod = st.ModTime()
    279 	}
    280 	if tag == "" || mod.IsZero() {
    281 		tag = ""
    282 		mod = time.Time{}
    283 	}
    284 	return
    285 }
    286