mirror of
https://github.com/reactos/syzkaller.git
synced 2024-11-23 19:39:40 +00:00
5153aeaffd
Boot and minimally test images before declaring them as good and switching to using them. If image build/boot/test fails, upload report about this to dashboard.
537 lines
13 KiB
Go
537 lines
13 KiB
Go
// 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 qemu
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"math/rand"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/syzkaller/pkg/config"
|
|
. "github.com/google/syzkaller/pkg/log"
|
|
"github.com/google/syzkaller/pkg/osutil"
|
|
"github.com/google/syzkaller/vm/vmimpl"
|
|
)
|
|
|
|
const (
|
|
hostAddr = "10.0.2.10"
|
|
)
|
|
|
|
func init() {
|
|
vmimpl.Register("qemu", ctor)
|
|
}
|
|
|
|
type Config struct {
|
|
Count int // number of VMs to use
|
|
Qemu string // qemu binary name (qemu-system-arch by default)
|
|
Qemu_Args string // additional command line arguments for qemu binary
|
|
Kernel string // kernel for injected boot (e.g. arch/x86/boot/bzImage)
|
|
Cmdline string // kernel command line (can only be specified with kernel)
|
|
Initrd string // linux initial ramdisk. (optional)
|
|
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
|
|
image string
|
|
debug bool
|
|
workdir string
|
|
sshkey string
|
|
sshuser string
|
|
port int
|
|
rpipe io.ReadCloser
|
|
wpipe io.WriteCloser
|
|
qemu *exec.Cmd
|
|
waiterC chan error
|
|
merger *vmimpl.OutputMerger
|
|
}
|
|
|
|
type archConfig struct {
|
|
Qemu string
|
|
QemuArgs string
|
|
}
|
|
|
|
var archConfigs = map[string]archConfig{
|
|
"linux/amd64": {
|
|
Qemu: "qemu-system-x86_64",
|
|
QemuArgs: "-enable-kvm -usb -usbdevice mouse -usbdevice tablet -soundhw all",
|
|
},
|
|
"linux/386": {
|
|
Qemu: "qemu-system-i386",
|
|
},
|
|
"linux/arm64": {
|
|
Qemu: "qemu-system-aarch64",
|
|
QemuArgs: "-machine virt -cpu cortex-a57",
|
|
},
|
|
"linux/arm": {
|
|
Qemu: "qemu-system-arm",
|
|
},
|
|
"linux/ppc64le": {
|
|
Qemu: "qemu-system-ppc64",
|
|
},
|
|
"freebsd/amd64": {
|
|
Qemu: "qemu-system-x86_64",
|
|
QemuArgs: "-enable-kvm -usb -usbdevice mouse -usbdevice tablet -soundhw all",
|
|
},
|
|
"netbsd/amd64": {
|
|
Qemu: "qemu-system-x86_64",
|
|
QemuArgs: "-enable-kvm -usb -usbdevice mouse -usbdevice tablet -soundhw all",
|
|
},
|
|
"fuchsia/amd64": {
|
|
Qemu: "qemu-system-x86_64",
|
|
QemuArgs: "-enable-kvm",
|
|
},
|
|
}
|
|
|
|
func ctor(env *vmimpl.Env) (vmimpl.Pool, error) {
|
|
archConfig := archConfigs[env.OS+"/"+env.Arch]
|
|
cfg := &Config{
|
|
Count: 1,
|
|
Qemu: archConfig.Qemu,
|
|
Qemu_Args: archConfig.QemuArgs,
|
|
}
|
|
if err := config.LoadData(env.Config, cfg); err != nil {
|
|
return nil, fmt.Errorf("failed to parse qemu 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 _, err := exec.LookPath(cfg.Qemu); err != nil {
|
|
return nil, err
|
|
}
|
|
if env.Image == "9p" {
|
|
if env.OS != "linux" {
|
|
return nil, fmt.Errorf("9p image is supported for linux only")
|
|
}
|
|
if cfg.Kernel == "" {
|
|
return nil, fmt.Errorf("9p image requires kernel")
|
|
}
|
|
} else {
|
|
if !osutil.IsExist(env.Image) {
|
|
return nil, fmt.Errorf("image file '%v' does not exist", env.Image)
|
|
}
|
|
if !osutil.IsExist(env.SshKey) {
|
|
return nil, fmt.Errorf("ssh key '%v' does not exist", env.SshKey)
|
|
}
|
|
}
|
|
if cfg.Cpu <= 0 || cfg.Cpu > 1024 {
|
|
return nil, fmt.Errorf("bad qemu cpu: %v, want [1-1024]", cfg.Cpu)
|
|
}
|
|
if cfg.Mem < 128 || cfg.Mem > 1048576 {
|
|
return nil, fmt.Errorf("bad qemu mem: %v, want [128-1048576]", cfg.Mem)
|
|
}
|
|
cfg.Kernel = osutil.Abs(cfg.Kernel)
|
|
cfg.Initrd = osutil.Abs(cfg.Initrd)
|
|
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) {
|
|
sshkey := pool.env.SshKey
|
|
sshuser := pool.env.SshUser
|
|
if pool.env.Image == "9p" {
|
|
sshkey = filepath.Join(workdir, "key")
|
|
sshuser = "root"
|
|
if _, err := osutil.RunCmd(10*time.Minute, "", "ssh-keygen", "-t", "rsa", "-b", "2048",
|
|
"-N", "", "-C", "", "-f", sshkey); err != nil {
|
|
return nil, err
|
|
}
|
|
initFile := filepath.Join(workdir, "init.sh")
|
|
if err := osutil.WriteExecFile(initFile, []byte(strings.Replace(initScript, "{{KEY}}", sshkey, -1))); err != nil {
|
|
return nil, fmt.Errorf("failed to create init file: %v", err)
|
|
}
|
|
}
|
|
|
|
for i := 0; ; i++ {
|
|
inst, err := pool.ctor(workdir, sshkey, sshuser, index)
|
|
if err == nil {
|
|
return inst, nil
|
|
}
|
|
// Older qemu prints "could", newer -- "Could".
|
|
if i < 1000 && strings.Contains(err.Error(), "ould not set up host forwarding rule") {
|
|
continue
|
|
}
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
func (pool *Pool) ctor(workdir, sshkey, sshuser string, index int) (vmimpl.Instance, error) {
|
|
inst := &instance{
|
|
cfg: pool.cfg,
|
|
image: pool.env.Image,
|
|
debug: pool.env.Debug,
|
|
workdir: workdir,
|
|
sshkey: sshkey,
|
|
sshuser: sshuser,
|
|
}
|
|
closeInst := inst
|
|
defer func() {
|
|
if closeInst != nil {
|
|
closeInst.Close()
|
|
}
|
|
}()
|
|
|
|
var err error
|
|
inst.rpipe, inst.wpipe, err = osutil.LongPipe()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := inst.Boot(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
closeInst = nil
|
|
return inst, nil
|
|
}
|
|
|
|
func (inst *instance) Close() {
|
|
if inst.qemu != nil {
|
|
inst.qemu.Process.Kill()
|
|
err := <-inst.waiterC
|
|
inst.waiterC <- err // repost it for waiting goroutines
|
|
}
|
|
if inst.merger != nil {
|
|
inst.merger.Wait()
|
|
}
|
|
if inst.rpipe != nil {
|
|
inst.rpipe.Close()
|
|
}
|
|
if inst.wpipe != nil {
|
|
inst.wpipe.Close()
|
|
}
|
|
}
|
|
|
|
func (inst *instance) Boot() error {
|
|
for {
|
|
// Find an unused TCP port.
|
|
inst.port = rand.Intn(64<<10-1<<10) + 1<<10
|
|
ln, err := net.Listen("tcp", fmt.Sprintf("localhost:%v", inst.port))
|
|
if err == nil {
|
|
ln.Close()
|
|
break
|
|
}
|
|
}
|
|
// TODO: ignores inst.cfg.Cpu
|
|
args := []string{
|
|
"-m", strconv.Itoa(inst.cfg.Mem),
|
|
"-net", "nic",
|
|
"-net", fmt.Sprintf("user,host=%v,hostfwd=tcp::%v-:22", hostAddr, inst.port),
|
|
"-display", "none",
|
|
"-serial", "stdio",
|
|
"-no-reboot",
|
|
"-numa", "node,nodeid=0,cpus=0-1", "-numa", "node,nodeid=1,cpus=2-3",
|
|
"-smp", "sockets=2,cores=2,threads=1",
|
|
}
|
|
args = append(args, strings.Split(inst.cfg.Qemu_Args, " ")...)
|
|
if inst.image == "9p" {
|
|
args = append(args,
|
|
"-fsdev", "local,id=fsdev0,path=/,security_model=none,readonly",
|
|
"-device", "virtio-9p-pci,fsdev=fsdev0,mount_tag=/dev/root",
|
|
)
|
|
} else {
|
|
args = append(args,
|
|
"-hda", inst.image,
|
|
"-snapshot",
|
|
)
|
|
}
|
|
if inst.cfg.Initrd != "" {
|
|
args = append(args,
|
|
"-initrd", inst.cfg.Initrd,
|
|
)
|
|
}
|
|
if inst.cfg.Kernel != "" {
|
|
cmdline := []string{
|
|
"console=ttyS0",
|
|
"vsyscall=native",
|
|
"rodata=n",
|
|
"oops=panic",
|
|
"nmi_watchdog=panic",
|
|
"panic_on_warn=1",
|
|
"panic=86400",
|
|
"ftrace_dump_on_oops=orig_cpu",
|
|
"earlyprintk=serial",
|
|
"net.ifnames=0",
|
|
"biosdevname=0",
|
|
"kvm-intel.nested=1",
|
|
"kvm-intel.unrestricted_guest=1",
|
|
"kvm-intel.vmm_exclusive=1",
|
|
"kvm-intel.fasteoi=1",
|
|
"kvm-intel.ept=1",
|
|
"kvm-intel.flexpriority=1",
|
|
"kvm-intel.vpid=1",
|
|
"kvm-intel.emulate_invalid_guest_state=1",
|
|
"kvm-intel.eptad=1",
|
|
"kvm-intel.enable_shadow_vmcs=1",
|
|
"kvm-intel.pml=1",
|
|
"kvm-intel.enable_apicv=1",
|
|
}
|
|
if inst.image == "9p" {
|
|
cmdline = append(cmdline,
|
|
"root=/dev/root",
|
|
"rootfstype=9p",
|
|
"rootflags=trans=virtio,version=9p2000.L,cache=loose",
|
|
"init="+filepath.Join(inst.workdir, "init.sh"),
|
|
)
|
|
} else {
|
|
cmdline = append(cmdline,
|
|
"root=/dev/sda",
|
|
)
|
|
}
|
|
cmdline = append(cmdline, inst.cfg.Cmdline)
|
|
args = append(args,
|
|
"-kernel", inst.cfg.Kernel,
|
|
"-append", strings.Join(cmdline, " "),
|
|
)
|
|
}
|
|
if inst.debug {
|
|
Logf(0, "running command: %v %#v", inst.cfg.Qemu, args)
|
|
}
|
|
qemu := osutil.Command(inst.cfg.Qemu, args...)
|
|
qemu.Stdout = inst.wpipe
|
|
qemu.Stderr = inst.wpipe
|
|
if err := qemu.Start(); err != nil {
|
|
return fmt.Errorf("failed to start %v %+v: %v", inst.cfg.Qemu, args, err)
|
|
}
|
|
inst.wpipe.Close()
|
|
inst.wpipe = nil
|
|
inst.qemu = qemu
|
|
// Qemu has started.
|
|
|
|
// Start output merger.
|
|
var tee io.Writer
|
|
if inst.debug {
|
|
tee = os.Stdout
|
|
}
|
|
inst.merger = vmimpl.NewOutputMerger(tee)
|
|
inst.merger.Add("qemu", inst.rpipe)
|
|
inst.rpipe = nil
|
|
|
|
var bootOutput []byte
|
|
bootOutputStop := make(chan bool)
|
|
go func() {
|
|
for {
|
|
select {
|
|
case out := <-inst.merger.Output:
|
|
bootOutput = append(bootOutput, out...)
|
|
case <-bootOutputStop:
|
|
close(bootOutputStop)
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Wait for the qemu asynchronously.
|
|
inst.waiterC = make(chan error, 1)
|
|
go func() {
|
|
err := qemu.Wait()
|
|
inst.waiterC <- err
|
|
}()
|
|
|
|
// Wait for ssh server to come up.
|
|
time.Sleep(5 * time.Second)
|
|
start := time.Now()
|
|
for {
|
|
c, err := net.DialTimeout("tcp", fmt.Sprintf("localhost:%v", inst.port), 1*time.Second)
|
|
if err == nil {
|
|
c.SetDeadline(time.Now().Add(1 * time.Second))
|
|
var tmp [1]byte
|
|
n, err := c.Read(tmp[:])
|
|
c.Close()
|
|
if err == nil && n > 0 {
|
|
break // ssh is up and responding
|
|
}
|
|
time.Sleep(3 * time.Second)
|
|
}
|
|
select {
|
|
case err := <-inst.waiterC:
|
|
inst.waiterC <- err // repost it for Close
|
|
time.Sleep(time.Second) // wait for any pending output
|
|
bootOutputStop <- true
|
|
<-bootOutputStop
|
|
return vmimpl.BootError{"qemu stopped", bootOutput}
|
|
default:
|
|
}
|
|
if time.Since(start) > 10*time.Minute {
|
|
bootOutputStop <- true
|
|
<-bootOutputStop
|
|
return vmimpl.BootError{"ssh server did not start", bootOutput}
|
|
}
|
|
}
|
|
bootOutputStop <- true
|
|
return nil
|
|
}
|
|
|
|
func (inst *instance) Forward(port int) (string, error) {
|
|
return fmt.Sprintf("%v:%v", hostAddr, port), nil
|
|
}
|
|
|
|
func (inst *instance) Copy(hostSrc string) (string, error) {
|
|
basePath := "/"
|
|
if inst.image == "9p" {
|
|
basePath = "/tmp"
|
|
}
|
|
vmDst := filepath.Join(basePath, filepath.Base(hostSrc))
|
|
args := append(inst.sshArgs("-P"), hostSrc, inst.sshuser+"@localhost:"+vmDst)
|
|
cmd := osutil.Command("scp", args...)
|
|
if inst.debug {
|
|
Logf(0, "running command: scp %#v", args)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stdout
|
|
}
|
|
if err := cmd.Start(); err != nil {
|
|
return "", err
|
|
}
|
|
done := make(chan bool)
|
|
go func() {
|
|
select {
|
|
case <-time.After(3 * time.Minute):
|
|
cmd.Process.Kill()
|
|
case <-done:
|
|
}
|
|
}()
|
|
err := cmd.Wait()
|
|
close(done)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return vmDst, nil
|
|
}
|
|
|
|
func (inst *instance) Run(timeout time.Duration, stop <-chan bool, command string) (<-chan []byte, <-chan error, error) {
|
|
rpipe, wpipe, err := osutil.LongPipe()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
inst.merger.Add("ssh", rpipe)
|
|
|
|
args := append(inst.sshArgs("-p"), inst.sshuser+"@localhost", command)
|
|
if inst.debug {
|
|
Logf(0, "running command: ssh %#v", args)
|
|
}
|
|
cmd := osutil.Command("ssh", args...)
|
|
cmd.Stdout = wpipe
|
|
cmd.Stderr = wpipe
|
|
if err := cmd.Start(); err != nil {
|
|
wpipe.Close()
|
|
return nil, nil, err
|
|
}
|
|
wpipe.Close()
|
|
errc := make(chan error, 1)
|
|
signal := func(err error) {
|
|
select {
|
|
case errc <- err:
|
|
default:
|
|
}
|
|
}
|
|
|
|
go func() {
|
|
select {
|
|
case <-time.After(timeout):
|
|
signal(vmimpl.TimeoutErr)
|
|
case <-stop:
|
|
signal(vmimpl.TimeoutErr)
|
|
case err := <-inst.merger.Err:
|
|
cmd.Process.Kill()
|
|
if cmdErr := cmd.Wait(); cmdErr == nil {
|
|
// If the command exited successfully, we got EOF error from merger.
|
|
// But in this case no error has happened and the EOF is expected.
|
|
err = nil
|
|
}
|
|
signal(err)
|
|
return
|
|
}
|
|
cmd.Process.Kill()
|
|
cmd.Wait()
|
|
}()
|
|
return inst.merger.Output, errc, nil
|
|
}
|
|
|
|
func (inst *instance) sshArgs(portArg string) []string {
|
|
args := []string{
|
|
"-i", inst.sshkey,
|
|
portArg, strconv.Itoa(inst.port),
|
|
"-F", "/dev/null",
|
|
"-o", "ConnectionAttempts=10",
|
|
"-o", "ConnectTimeout=10",
|
|
"-o", "BatchMode=yes",
|
|
"-o", "UserKnownHostsFile=/dev/null",
|
|
"-o", "IdentitiesOnly=yes",
|
|
"-o", "StrictHostKeyChecking=no",
|
|
"-o", "LogLevel=error",
|
|
}
|
|
if inst.debug {
|
|
args = append(args, "-v")
|
|
}
|
|
return args
|
|
}
|
|
|
|
const initScript = `#! /bin/bash
|
|
set -eux
|
|
mount -t proc none /proc
|
|
mount -t sysfs none /sys
|
|
mount -t debugfs nodev /sys/kernel/debug/
|
|
mount -t tmpfs none /tmp
|
|
mount -t tmpfs none /var
|
|
mount -t tmpfs none /run
|
|
mount -t tmpfs none /etc
|
|
mount -t tmpfs none /root
|
|
touch /etc/fstab
|
|
mkdir /etc/network
|
|
mkdir /run/network
|
|
printf 'auto lo\niface lo inet loopback\n\n' >> /etc/network/interfaces
|
|
printf 'auto eth0\niface eth0 inet static\naddress 10.0.2.15\nnetmask 255.255.255.0\nnetwork 10.0.2.0\ngateway 10.0.2.1\nbroadcast 10.0.2.255\n\n' >> /etc/network/interfaces
|
|
printf 'auto eth0\niface eth0 inet6 static\naddress fe80::5054:ff:fe12:3456/64\ngateway 2000:da8:203:612:0:3:0:1\n\n' >> /etc/network/interfaces
|
|
ifup lo
|
|
ifup eth0
|
|
echo "root::0:0:root:/root:/bin/bash" > /etc/passwd
|
|
mkdir -p /etc/ssh
|
|
cp {{KEY}}.pub /root/
|
|
chmod 0700 /root
|
|
chmod 0600 /root/key.pub
|
|
mkdir -p /var/run/sshd/
|
|
chmod 700 /var/run/sshd
|
|
cat > /etc/ssh/sshd_config <<EOF
|
|
Port 22
|
|
Protocol 2
|
|
UsePrivilegeSeparation no
|
|
HostKey {{KEY}}
|
|
PermitRootLogin yes
|
|
AuthenticationMethods publickey
|
|
ChallengeResponseAuthentication no
|
|
AuthorizedKeysFile /root/key.pub
|
|
IgnoreUserKnownHosts yes
|
|
AllowUsers root
|
|
LogLevel INFO
|
|
TCPKeepAlive yes
|
|
RSAAuthentication yes
|
|
PubkeyAuthentication yes
|
|
EOF
|
|
/usr/sbin/sshd -e -D
|
|
/sbin/halt -f
|
|
`
|