• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2015 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 kvm provides VMs based on lkvm (kvmtool) virtualization.
5// It is not well tested.
6package kvm
7
8import (
9	"fmt"
10	"os"
11	"os/exec"
12	"path/filepath"
13	"runtime"
14	"strconv"
15	"sync"
16	"time"
17
18	"github.com/google/syzkaller/pkg/config"
19	"github.com/google/syzkaller/pkg/osutil"
20	"github.com/google/syzkaller/vm/vmimpl"
21)
22
23const (
24	hostAddr = "192.168.33.1"
25)
26
27func init() {
28	vmimpl.Register("kvm", ctor)
29}
30
31type Config struct {
32	Count   int    // number of VMs to use
33	Lkvm    string // lkvm binary name
34	Kernel  string // e.g. arch/x86/boot/bzImage
35	Cmdline string // kernel command line
36	CPU     int    // number of VM CPUs
37	Mem     int    // amount of VM memory in MBs
38}
39
40type Pool struct {
41	env *vmimpl.Env
42	cfg *Config
43}
44
45type instance struct {
46	cfg         *Config
47	sandbox     string
48	sandboxPath string
49	lkvm        *exec.Cmd
50	readerC     chan error
51	waiterC     chan error
52	debug       bool
53
54	mu      sync.Mutex
55	outputB []byte
56	outputC chan []byte
57}
58
59func ctor(env *vmimpl.Env) (vmimpl.Pool, error) {
60	cfg := &Config{
61		Count: 1,
62		Lkvm:  "lkvm",
63	}
64	if err := config.LoadData(env.Config, cfg); err != nil {
65		return nil, fmt.Errorf("failed to parse kvm vm config: %v", err)
66	}
67	if cfg.Count < 1 || cfg.Count > 1000 {
68		return nil, fmt.Errorf("invalid config param count: %v, want [1, 1000]", cfg.Count)
69	}
70	if env.Debug {
71		cfg.Count = 1
72	}
73	if env.Image != "" {
74		return nil, fmt.Errorf("lkvm does not support custom images")
75	}
76	if _, err := exec.LookPath(cfg.Lkvm); err != nil {
77		return nil, err
78	}
79	if !osutil.IsExist(cfg.Kernel) {
80		return nil, fmt.Errorf("kernel file '%v' does not exist", cfg.Kernel)
81	}
82	if cfg.CPU < 1 || cfg.CPU > 1024 {
83		return nil, fmt.Errorf("invalid config param cpu: %v, want [1-1024]", cfg.CPU)
84	}
85	if cfg.Mem < 128 || cfg.Mem > 1048576 {
86		return nil, fmt.Errorf("invalid config param mem: %v, want [128-1048576]", cfg.Mem)
87	}
88	cfg.Kernel = osutil.Abs(cfg.Kernel)
89	pool := &Pool{
90		cfg: cfg,
91		env: env,
92	}
93	return pool, nil
94}
95
96func (pool *Pool) Count() int {
97	return pool.cfg.Count
98}
99
100func (pool *Pool) Create(workdir string, index int) (vmimpl.Instance, error) {
101	sandbox := fmt.Sprintf("syz-%v", index)
102	inst := &instance{
103		cfg:         pool.cfg,
104		sandbox:     sandbox,
105		sandboxPath: filepath.Join(os.Getenv("HOME"), ".lkvm", sandbox),
106		debug:       pool.env.Debug,
107	}
108	closeInst := inst
109	defer func() {
110		if closeInst != nil {
111			closeInst.Close()
112		}
113	}()
114
115	os.RemoveAll(inst.sandboxPath)
116	os.Remove(inst.sandboxPath + ".sock")
117	out, err := osutil.Command(inst.cfg.Lkvm, "setup", sandbox).CombinedOutput()
118	if err != nil {
119		return nil, fmt.Errorf("failed to lkvm setup: %v\n%s", err, out)
120	}
121	scriptPath := filepath.Join(workdir, "script.sh")
122	if err := osutil.WriteExecFile(scriptPath, []byte(script)); err != nil {
123		return nil, fmt.Errorf("failed to create temp file: %v", err)
124	}
125
126	rpipe, wpipe, err := osutil.LongPipe()
127	if err != nil {
128		return nil, fmt.Errorf("failed to create pipe: %v", err)
129	}
130
131	inst.lkvm = osutil.Command("taskset", "-c", strconv.Itoa(index%runtime.NumCPU()),
132		inst.cfg.Lkvm, "sandbox",
133		"--disk", inst.sandbox,
134		"--kernel", inst.cfg.Kernel,
135		"--params", "slub_debug=UZ "+inst.cfg.Cmdline,
136		"--mem", strconv.Itoa(inst.cfg.Mem),
137		"--cpus", strconv.Itoa(inst.cfg.CPU),
138		"--network", "mode=user",
139		"--sandbox", scriptPath,
140	)
141	inst.lkvm.Stdout = wpipe
142	inst.lkvm.Stderr = wpipe
143	if err := inst.lkvm.Start(); err != nil {
144		rpipe.Close()
145		wpipe.Close()
146		return nil, fmt.Errorf("failed to start lkvm: %v", err)
147	}
148
149	// Start output reading goroutine.
150	inst.readerC = make(chan error)
151	go func() {
152		var buf [64 << 10]byte
153		for {
154			n, err := rpipe.Read(buf[:])
155			if n != 0 {
156				if inst.debug {
157					os.Stdout.Write(buf[:n])
158					os.Stdout.Write([]byte{'\n'})
159				}
160				inst.mu.Lock()
161				inst.outputB = append(inst.outputB, buf[:n]...)
162				if inst.outputC != nil {
163					select {
164					case inst.outputC <- inst.outputB:
165						inst.outputB = nil
166					default:
167					}
168				}
169				inst.mu.Unlock()
170				time.Sleep(time.Millisecond)
171			}
172			if err != nil {
173				rpipe.Close()
174				inst.readerC <- err
175				return
176			}
177		}
178	}()
179
180	// Wait for the lkvm asynchronously.
181	inst.waiterC = make(chan error, 1)
182	go func() {
183		err := inst.lkvm.Wait()
184		wpipe.Close()
185		inst.waiterC <- err
186	}()
187
188	// Wait for the script to start serving.
189	_, errc, err := inst.Run(10*time.Minute, nil, "mount -t debugfs none /sys/kernel/debug/")
190	if err == nil {
191		err = <-errc
192	}
193	if err != nil {
194		return nil, fmt.Errorf("failed to run script: %v", err)
195	}
196
197	closeInst = nil
198	return inst, nil
199}
200
201func (inst *instance) Close() {
202	if inst.lkvm != nil {
203		inst.lkvm.Process.Kill()
204		err := <-inst.waiterC
205		inst.waiterC <- err // repost it for waiting goroutines
206		<-inst.readerC
207	}
208	os.RemoveAll(inst.sandboxPath)
209	os.Remove(inst.sandboxPath + ".sock")
210}
211
212func (inst *instance) Forward(port int) (string, error) {
213	return fmt.Sprintf("%v:%v", hostAddr, port), nil
214}
215
216func (inst *instance) Copy(hostSrc string) (string, error) {
217	vmDst := filepath.Join("/", filepath.Base(hostSrc))
218	dst := filepath.Join(inst.sandboxPath, vmDst)
219	if err := osutil.CopyFile(hostSrc, dst); err != nil {
220		return "", err
221	}
222	if err := os.Chmod(dst, 0777); err != nil {
223		return "", err
224	}
225	return vmDst, nil
226}
227
228func (inst *instance) Run(timeout time.Duration, stop <-chan bool, command string) (
229	<-chan []byte, <-chan error, error) {
230	outputC := make(chan []byte, 10)
231	errorC := make(chan error, 1)
232	inst.mu.Lock()
233	inst.outputB = nil
234	inst.outputC = outputC
235	inst.mu.Unlock()
236
237	cmdFile := filepath.Join(inst.sandboxPath, "/syz-cmd")
238	tmpFile := cmdFile + "-tmp"
239	if err := osutil.WriteExecFile(tmpFile, []byte(command)); err != nil {
240		return nil, nil, err
241	}
242	if err := os.Rename(tmpFile, cmdFile); err != nil {
243		return nil, nil, err
244	}
245
246	signal := func(err error) {
247		inst.mu.Lock()
248		if inst.outputC == outputC {
249			inst.outputB = nil
250			inst.outputC = nil
251		}
252		inst.mu.Unlock()
253		errorC <- err
254	}
255
256	go func() {
257		timeoutTicker := time.NewTicker(timeout)
258		secondTicker := time.NewTicker(time.Second)
259		var resultErr error
260	loop:
261		for {
262			select {
263			case <-timeoutTicker.C:
264				resultErr = vmimpl.ErrTimeout
265				break loop
266			case <-stop:
267				resultErr = vmimpl.ErrTimeout
268				break loop
269			case <-secondTicker.C:
270				if !osutil.IsExist(cmdFile) {
271					resultErr = nil
272					break loop
273				}
274			case err := <-inst.waiterC:
275				inst.waiterC <- err // repost it for Close
276				resultErr = fmt.Errorf("lkvm exited")
277				break loop
278			}
279		}
280		signal(resultErr)
281		timeoutTicker.Stop()
282		secondTicker.Stop()
283	}()
284
285	return outputC, errorC, nil
286}
287
288func (inst *instance) Diagnose() bool {
289	return false
290}
291
292const script = `#! /bin/bash
293while true; do
294	if [ -e "/syz-cmd" ]; then
295		/syz-cmd
296		rm -f /syz-cmd
297	else
298		sleep 1
299	fi
300done
301`
302