// Copyright 2015 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 kvm provides VMs based on lkvm (kvmtool) virtualization. // It is not well tested. package kvm import ( "fmt" "os" "os/exec" "path/filepath" "runtime" "strconv" "sync" "time" "github.com/google/syzkaller/pkg/config" "github.com/google/syzkaller/pkg/osutil" "github.com/google/syzkaller/vm/vmimpl" ) const ( hostAddr = "192.168.33.1" ) func init() { vmimpl.Register("kvm", ctor) } type Config struct { Count int // number of VMs to use Lkvm string // lkvm binary name Kernel string // e.g. arch/x86/boot/bzImage Cmdline string // kernel command line CPU int // number of VM CPUs Mem int // amount of VM memory in MBs } type Pool struct { env *vmimpl.Env cfg *Config } type instance struct { cfg *Config sandbox string sandboxPath string lkvm *exec.Cmd readerC chan error waiterC chan error debug bool mu sync.Mutex outputB []byte outputC chan []byte } func ctor(env *vmimpl.Env) (vmimpl.Pool, error) { cfg := &Config{ Count: 1, Lkvm: "lkvm", } if err := config.LoadData(env.Config, cfg); err != nil { return nil, fmt.Errorf("failed to parse kvm vm config: %v", err) } if cfg.Count < 1 || cfg.Count > 1000 { return nil, fmt.Errorf("invalid config param count: %v, want [1, 1000]", cfg.Count) } if env.Debug { cfg.Count = 1 } if env.Image != "" { return nil, fmt.Errorf("lkvm does not support custom images") } if _, err := exec.LookPath(cfg.Lkvm); err != nil { return nil, err } if !osutil.IsExist(cfg.Kernel) { return nil, fmt.Errorf("kernel file '%v' does not exist", cfg.Kernel) } if cfg.CPU < 1 || cfg.CPU > 1024 { return nil, fmt.Errorf("invalid config param cpu: %v, want [1-1024]", cfg.CPU) } if cfg.Mem < 128 || cfg.Mem > 1048576 { return nil, fmt.Errorf("invalid config param mem: %v, want [128-1048576]", cfg.Mem) } cfg.Kernel = osutil.Abs(cfg.Kernel) pool := &Pool{ cfg: cfg, env: env, } return pool, nil } func (pool *Pool) Count() int { return pool.cfg.Count } func (pool *Pool) Create(workdir string, index int) (vmimpl.Instance, error) { sandbox := fmt.Sprintf("syz-%v", index) inst := &instance{ cfg: pool.cfg, sandbox: sandbox, sandboxPath: filepath.Join(os.Getenv("HOME"), ".lkvm", sandbox), debug: pool.env.Debug, } closeInst := inst defer func() { if closeInst != nil { closeInst.Close() } }() os.RemoveAll(inst.sandboxPath) os.Remove(inst.sandboxPath + ".sock") out, err := osutil.Command(inst.cfg.Lkvm, "setup", sandbox).CombinedOutput() if err != nil { return nil, fmt.Errorf("failed to lkvm setup: %v\n%s", err, out) } scriptPath := filepath.Join(workdir, "script.sh") if err := osutil.WriteExecFile(scriptPath, []byte(script)); err != nil { return nil, fmt.Errorf("failed to create temp file: %v", err) } rpipe, wpipe, err := osutil.LongPipe() if err != nil { return nil, fmt.Errorf("failed to create pipe: %v", err) } inst.lkvm = osutil.Command("taskset", "-c", strconv.Itoa(index%runtime.NumCPU()), inst.cfg.Lkvm, "sandbox", "--disk", inst.sandbox, "--kernel", inst.cfg.Kernel, "--params", "slub_debug=UZ "+inst.cfg.Cmdline, "--mem", strconv.Itoa(inst.cfg.Mem), "--cpus", strconv.Itoa(inst.cfg.CPU), "--network", "mode=user", "--sandbox", scriptPath, ) inst.lkvm.Stdout = wpipe inst.lkvm.Stderr = wpipe if err := inst.lkvm.Start(); err != nil { rpipe.Close() wpipe.Close() return nil, fmt.Errorf("failed to start lkvm: %v", err) } // Start output reading goroutine. inst.readerC = make(chan error) go func() { var buf [64 << 10]byte for { n, err := rpipe.Read(buf[:]) if n != 0 { if inst.debug { os.Stdout.Write(buf[:n]) os.Stdout.Write([]byte{'\n'}) } inst.mu.Lock() inst.outputB = append(inst.outputB, buf[:n]...) if inst.outputC != nil { select { case inst.outputC <- inst.outputB: inst.outputB = nil default: } } inst.mu.Unlock() time.Sleep(time.Millisecond) } if err != nil { rpipe.Close() inst.readerC <- err return } } }() // Wait for the lkvm asynchronously. inst.waiterC = make(chan error, 1) go func() { err := inst.lkvm.Wait() wpipe.Close() inst.waiterC <- err }() // Wait for the script to start serving. _, errc, err := inst.Run(10*time.Minute, nil, "mount -t debugfs none /sys/kernel/debug/") if err == nil { err = <-errc } if err != nil { return nil, fmt.Errorf("failed to run script: %v", err) } closeInst = nil return inst, nil } func (inst *instance) Close() { if inst.lkvm != nil { inst.lkvm.Process.Kill() err := <-inst.waiterC inst.waiterC <- err // repost it for waiting goroutines <-inst.readerC } os.RemoveAll(inst.sandboxPath) os.Remove(inst.sandboxPath + ".sock") } func (inst *instance) Forward(port int) (string, error) { return fmt.Sprintf("%v:%v", hostAddr, port), nil } func (inst *instance) Copy(hostSrc string) (string, error) { vmDst := filepath.Join("/", filepath.Base(hostSrc)) dst := filepath.Join(inst.sandboxPath, vmDst) if err := osutil.CopyFile(hostSrc, dst); err != nil { return "", err } if err := os.Chmod(dst, 0777); err != nil { return "", err } return vmDst, nil } func (inst *instance) Run(timeout time.Duration, stop <-chan bool, command string) ( <-chan []byte, <-chan error, error) { outputC := make(chan []byte, 10) errorC := make(chan error, 1) inst.mu.Lock() inst.outputB = nil inst.outputC = outputC inst.mu.Unlock() cmdFile := filepath.Join(inst.sandboxPath, "/syz-cmd") tmpFile := cmdFile + "-tmp" if err := osutil.WriteExecFile(tmpFile, []byte(command)); err != nil { return nil, nil, err } if err := os.Rename(tmpFile, cmdFile); err != nil { return nil, nil, err } signal := func(err error) { inst.mu.Lock() if inst.outputC == outputC { inst.outputB = nil inst.outputC = nil } inst.mu.Unlock() errorC <- err } go func() { timeoutTicker := time.NewTicker(timeout) secondTicker := time.NewTicker(time.Second) var resultErr error loop: for { select { case <-timeoutTicker.C: resultErr = vmimpl.ErrTimeout break loop case <-stop: resultErr = vmimpl.ErrTimeout break loop case <-secondTicker.C: if !osutil.IsExist(cmdFile) { resultErr = nil break loop } case err := <-inst.waiterC: inst.waiterC <- err // repost it for Close resultErr = fmt.Errorf("lkvm exited") break loop } } signal(resultErr) timeoutTicker.Stop() secondTicker.Stop() }() return outputC, errorC, nil } func (inst *instance) Diagnose() bool { return false } const script = `#! /bin/bash while true; do if [ -e "/syz-cmd" ]; then /syz-cmd rm -f /syz-cmd else sleep 1 fi done `