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
4package main
5
6import (
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
21const (
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.
32type 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
46func 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
109func (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.
158func (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.
174func (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
190func (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
207func (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.
266func (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
274func 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