• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2018 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 instance provides helper functions for creation of temporal instances
5// used for testing of images, patches and bisection.
6package instance
7
8import (
9	"bytes"
10	"encoding/json"
11	"fmt"
12	"net"
13	"os"
14	"path/filepath"
15	"strings"
16	"time"
17
18	"github.com/google/syzkaller/pkg/build"
19	"github.com/google/syzkaller/pkg/csource"
20	"github.com/google/syzkaller/pkg/log"
21	"github.com/google/syzkaller/pkg/mgrconfig"
22	"github.com/google/syzkaller/pkg/osutil"
23	"github.com/google/syzkaller/pkg/report"
24	"github.com/google/syzkaller/pkg/vcs"
25	"github.com/google/syzkaller/prog"
26	"github.com/google/syzkaller/vm"
27)
28
29type Env struct {
30	cfg *mgrconfig.Config
31}
32
33func NewEnv(cfg *mgrconfig.Config) (*Env, error) {
34	switch cfg.Type {
35	case "gce", "qemu", "gvisor":
36	default:
37		return nil, fmt.Errorf("test instances can only work with qemu/gce")
38	}
39	if cfg.Workdir == "" {
40		return nil, fmt.Errorf("workdir path is empty")
41	}
42	if cfg.KernelSrc == "" {
43		return nil, fmt.Errorf("kernel src path is empty")
44	}
45	if cfg.Syzkaller == "" {
46		return nil, fmt.Errorf("syzkaller path is empty")
47	}
48	if err := osutil.MkdirAll(cfg.Workdir); err != nil {
49		return nil, fmt.Errorf("failed to create tmp dir: %v", err)
50	}
51	env := &Env{
52		cfg: cfg,
53	}
54	return env, nil
55}
56
57func (env *Env) BuildSyzkaller(repo, commit string) error {
58	cfg := env.cfg
59	srcIndex := strings.LastIndex(cfg.Syzkaller, "/src/")
60	if srcIndex == -1 {
61		return fmt.Errorf("syzkaller path %q is not in GOPATH", cfg.Syzkaller)
62	}
63	if _, err := vcs.NewSyzkallerRepo(cfg.Syzkaller).CheckoutCommit(repo, commit); err != nil {
64		return fmt.Errorf("failed to checkout syzkaller repo: %v", err)
65	}
66	cmd := osutil.Command("make", "target")
67	cmd.Dir = cfg.Syzkaller
68	cmd.Env = append([]string{}, os.Environ()...)
69	cmd.Env = append(cmd.Env,
70		"GOPATH="+cfg.Syzkaller[:srcIndex],
71		"TARGETOS="+cfg.TargetOS,
72		"TARGETVMARCH="+cfg.TargetVMArch,
73		"TARGETARCH="+cfg.TargetArch,
74	)
75	if _, err := osutil.Run(time.Hour, cmd); err != nil {
76		return fmt.Errorf("syzkaller build failed: %v", err)
77	}
78	return nil
79}
80
81func (env *Env) BuildKernel(compilerBin, userspaceDir, cmdlineFile, sysctlFile string, kernelConfig []byte) error {
82	cfg := env.cfg
83	imageDir := filepath.Join(cfg.Workdir, "image")
84	if err := build.Image(cfg.TargetOS, cfg.TargetVMArch, cfg.Type,
85		cfg.KernelSrc, imageDir, compilerBin, userspaceDir,
86		cmdlineFile, sysctlFile, kernelConfig); err != nil {
87		return err
88	}
89	return SetConfigImage(cfg, imageDir)
90}
91
92func SetConfigImage(cfg *mgrconfig.Config, imageDir string) error {
93	cfg.KernelObj = filepath.Join(imageDir, "obj")
94	cfg.Image = filepath.Join(imageDir, "image")
95	if keyFile := filepath.Join(imageDir, "key"); osutil.IsExist(keyFile) {
96		cfg.SSHKey = keyFile
97	}
98	if cfg.Type == "qemu" {
99		kernel := filepath.Join(imageDir, "kernel")
100		if !osutil.IsExist(kernel) {
101			kernel = ""
102		}
103		initrd := filepath.Join(imageDir, "initrd")
104		if !osutil.IsExist(initrd) {
105			initrd = ""
106		}
107		if kernel != "" || initrd != "" {
108			qemu := make(map[string]interface{})
109			if err := json.Unmarshal(cfg.VM, &qemu); err != nil {
110				return fmt.Errorf("failed to parse qemu config: %v", err)
111			}
112			if kernel != "" {
113				qemu["kernel"] = kernel
114			}
115			if initrd != "" {
116				qemu["initrd"] = initrd
117			}
118			vmCfg, err := json.Marshal(qemu)
119			if err != nil {
120				return fmt.Errorf("failed to serialize qemu config: %v", err)
121			}
122			cfg.VM = vmCfg
123		}
124	}
125	return nil
126}
127
128type TestError struct {
129	Boot   bool // says if the error happened during booting or during instance testing
130	Title  string
131	Output []byte
132	Report *report.Report
133}
134
135func (err *TestError) Error() string {
136	return err.Title
137}
138
139type CrashError struct {
140	Report *report.Report
141}
142
143func (err *CrashError) Error() string {
144	return err.Report.Title
145}
146
147// Test boots numVMs VMs, tests basic kernel operation, and optionally tests the provided reproducer.
148// TestError is returned if there is a problem with kernel/image (crash, reboot loop, etc).
149// CrashError is returned if the reproducer crashes kernel.
150func (env *Env) Test(numVMs int, reproSyz, reproOpts, reproC []byte) ([]error, error) {
151	if err := mgrconfig.Complete(env.cfg); err != nil {
152		return nil, err
153	}
154	reporter, err := report.NewReporter(env.cfg)
155	if err != nil {
156		return nil, err
157	}
158	vmPool, err := vm.Create(env.cfg, false)
159	if err != nil {
160		return nil, fmt.Errorf("failed to create VM pool: %v", err)
161	}
162	if n := vmPool.Count(); numVMs > n {
163		numVMs = n
164	}
165	res := make(chan error, numVMs)
166	for i := 0; i < numVMs; i++ {
167		inst := &inst{
168			cfg:       env.cfg,
169			reporter:  reporter,
170			vmPool:    vmPool,
171			vmIndex:   i,
172			reproSyz:  reproSyz,
173			reproOpts: reproOpts,
174			reproC:    reproC,
175		}
176		go func() { res <- inst.test() }()
177	}
178	var errors []error
179	for i := 0; i < numVMs; i++ {
180		errors = append(errors, <-res)
181	}
182	return errors, nil
183}
184
185type inst struct {
186	cfg       *mgrconfig.Config
187	reporter  report.Reporter
188	vmPool    *vm.Pool
189	vm        *vm.Instance
190	vmIndex   int
191	reproSyz  []byte
192	reproOpts []byte
193	reproC    []byte
194}
195
196func (inst *inst) test() error {
197	vmInst, err := inst.vmPool.Create(inst.vmIndex)
198	if err != nil {
199		testErr := &TestError{
200			Boot:  true,
201			Title: err.Error(),
202		}
203		if bootErr, ok := err.(vm.BootErrorer); ok {
204			testErr.Title, testErr.Output = bootErr.BootError()
205			// This linux-ism avoids detecting any crash during boot as "unexpected kernel reboot".
206			output := testErr.Output
207			if pos := bytes.Index(output, []byte("Booting the kernel.")); pos != -1 {
208				output = output[pos+1:]
209			}
210			testErr.Report = inst.reporter.Parse(output)
211			if testErr.Report != nil {
212				testErr.Title = testErr.Report.Title
213			} else {
214				testErr.Report = &report.Report{
215					Title:  testErr.Title,
216					Output: testErr.Output,
217				}
218			}
219			if err := inst.reporter.Symbolize(testErr.Report); err != nil {
220				// TODO(dvyukov): send such errors to dashboard.
221				log.Logf(0, "failed to symbolize report: %v", err)
222			}
223		}
224		return testErr
225	}
226	defer vmInst.Close()
227	inst.vm = vmInst
228	if err := inst.testInstance(); err != nil {
229		return err
230	}
231	if len(inst.reproSyz) != 0 {
232		if err := inst.testRepro(); err != nil {
233			return err
234		}
235	}
236	return nil
237}
238
239// testInstance tests basic operation of the provided VM
240// (that we can copy binaries, run binaries, they can connect to host, run syzkaller programs, etc).
241// TestError is returned if there is a problem with the kernel (e.g. crash).
242func (inst *inst) testInstance() error {
243	ln, err := net.Listen("tcp", ":")
244	if err != nil {
245		return fmt.Errorf("failed to open listening socket: %v", err)
246	}
247	defer ln.Close()
248	acceptErr := make(chan error, 1)
249	go func() {
250		conn, err := ln.Accept()
251		if err == nil {
252			conn.Close()
253		}
254		acceptErr <- err
255	}()
256	fwdAddr, err := inst.vm.Forward(ln.Addr().(*net.TCPAddr).Port)
257	if err != nil {
258		return fmt.Errorf("failed to setup port forwarding: %v", err)
259	}
260	fuzzerBin, err := inst.vm.Copy(inst.cfg.SyzFuzzerBin)
261	if err != nil {
262		return &TestError{Title: fmt.Sprintf("failed to copy test binary to VM: %v", err)}
263	}
264	executorBin, err := inst.vm.Copy(inst.cfg.SyzExecutorBin)
265	if err != nil {
266		return &TestError{Title: fmt.Sprintf("failed to copy test binary to VM: %v", err)}
267	}
268
269	cmd := FuzzerCmd(fuzzerBin, executorBin, "test", inst.cfg.TargetOS, inst.cfg.TargetArch, fwdAddr,
270		inst.cfg.Sandbox, 0, 0, false, false, true, false)
271	outc, errc, err := inst.vm.Run(5*time.Minute, nil, cmd)
272	if err != nil {
273		return fmt.Errorf("failed to run binary in VM: %v", err)
274	}
275	rep := inst.vm.MonitorExecution(outc, errc, inst.reporter, true)
276	if rep != nil {
277		if err := inst.reporter.Symbolize(rep); err != nil {
278			// TODO(dvyukov): send such errors to dashboard.
279			log.Logf(0, "failed to symbolize report: %v", err)
280		}
281		return &TestError{
282			Title:  rep.Title,
283			Report: rep,
284		}
285	}
286	select {
287	case err := <-acceptErr:
288		return err
289	case <-time.After(10 * time.Second):
290		return fmt.Errorf("test machine failed to connect to host")
291	}
292}
293
294func (inst *inst) testRepro() error {
295	cfg := inst.cfg
296	execprogBin, err := inst.vm.Copy(cfg.SyzExecprogBin)
297	if err != nil {
298		return &TestError{Title: fmt.Sprintf("failed to copy test binary to VM: %v", err)}
299	}
300	executorBin, err := inst.vm.Copy(cfg.SyzExecutorBin)
301	if err != nil {
302		return &TestError{Title: fmt.Sprintf("failed to copy test binary to VM: %v", err)}
303	}
304	progFile := filepath.Join(cfg.Workdir, "repro.prog")
305	if err := osutil.WriteFile(progFile, inst.reproSyz); err != nil {
306		return fmt.Errorf("failed to write temp file: %v", err)
307	}
308	vmProgFile, err := inst.vm.Copy(progFile)
309	if err != nil {
310		return &TestError{Title: fmt.Sprintf("failed to copy test binary to VM: %v", err)}
311	}
312	opts, err := csource.DeserializeOptions(inst.reproOpts)
313	if err != nil {
314		return err
315	}
316	// Combine repro options and default options in a way that increases chances to reproduce the crash.
317	// First, we always enable threaded/collide as it should be [almost] strictly better.
318	// Executor does not support empty sandbox, so we use none instead.
319	// Finally, always use repeat and multiple procs.
320	if opts.Sandbox == "" {
321		opts.Sandbox = "none"
322	}
323	if !opts.Fault {
324		opts.FaultCall = -1
325	}
326	cmdSyz := ExecprogCmd(execprogBin, executorBin, cfg.TargetOS, cfg.TargetArch, opts.Sandbox,
327		true, true, true, cfg.Procs, opts.FaultCall, opts.FaultNth, vmProgFile)
328	if err := inst.testProgram(cmdSyz, 7*time.Minute); err != nil {
329		return err
330	}
331	if len(inst.reproC) == 0 {
332		return nil
333	}
334	target, err := prog.GetTarget(cfg.TargetOS, cfg.TargetArch)
335	if err != nil {
336		return err
337	}
338	bin, err := csource.Build(target, inst.reproC)
339	if err != nil {
340		return err
341	}
342	vmBin, err := inst.vm.Copy(bin)
343	if err != nil {
344		return &TestError{Title: fmt.Sprintf("failed to copy test binary to VM: %v", err)}
345	}
346	// We should test for longer (e.g. 5 mins), but the problem is that
347	// reproducer does not print anything, so after 3 mins we detect "no output".
348	return inst.testProgram(vmBin, time.Minute)
349}
350
351func (inst *inst) testProgram(command string, testTime time.Duration) error {
352	outc, errc, err := inst.vm.Run(testTime, nil, command)
353	if err != nil {
354		return fmt.Errorf("failed to run binary in VM: %v", err)
355	}
356	rep := inst.vm.MonitorExecution(outc, errc, inst.reporter, true)
357	if rep == nil {
358		return nil
359	}
360	if err := inst.reporter.Symbolize(rep); err != nil {
361		log.Logf(0, "failed to symbolize report: %v", err)
362	}
363	return &CrashError{Report: rep}
364}
365
366func FuzzerCmd(fuzzer, executor, name, OS, arch, fwdAddr, sandbox string, procs, verbosity int,
367	cover, debug, test, runtest bool) string {
368	osArg := ""
369	if OS == "akaros" {
370		// Only akaros needs OS, because the rest assume host OS.
371		// But speciying OS for all OSes breaks patch testing on syzbot
372		// because old execprog does not have os flag.
373		osArg = " -os=" + OS
374	}
375	return fmt.Sprintf("%v -executor=%v -name=%v -arch=%v%v -manager=%v -sandbox=%v"+
376		" -procs=%v -v=%d -cover=%v -debug=%v -test=%v -runtest=%v",
377		fuzzer, executor, name, arch, osArg, fwdAddr, sandbox,
378		procs, verbosity, cover, debug, test, runtest)
379}
380
381func ExecprogCmd(execprog, executor, OS, arch, sandbox string, repeat, threaded, collide bool,
382	procs, faultCall, faultNth int, progFile string) string {
383	repeatCount := 1
384	if repeat {
385		repeatCount = 0
386	}
387	osArg := ""
388	if OS == "akaros" {
389		osArg = " -os=" + OS
390	}
391	return fmt.Sprintf("%v -executor=%v -arch=%v%v -sandbox=%v"+
392		" -procs=%v -repeat=%v -threaded=%v -collide=%v -cover=0"+
393		" -fault_call=%v -fault_nth=%v %v",
394		execprog, executor, arch, osArg, sandbox,
395		procs, repeatCount, threaded, collide,
396		faultCall, faultNth, progFile)
397}
398