| // 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 ( |
| "bytes" |
| "encoding/json" |
| "fmt" |
| "io" |
| "net" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "regexp" |
| "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/pkg/report" |
| "github.com/google/syzkaller/sys/targets" |
| "github.com/google/syzkaller/vm/vmimpl" |
| ) |
| |
| func init() { |
| var _ vmimpl.Infoer = (*instance)(nil) |
| vmimpl.Register("qemu", vmimpl.Type{ |
| Ctor: ctor, |
| Overcommit: true, |
| }) |
| } |
| |
| type Config struct { |
| // Number of VMs to run in parallel (1 by default). |
| Count int `json:"count"` |
| // QEMU binary name (optional). |
| // If not specified, qemu-system-arch is used by default. |
| Qemu string `json:"qemu"` |
| // Additional command line arguments for the QEMU binary. |
| // If not specified, the default value specifies machine type, cpu and usually contains -enable-kvm. |
| // If you provide this parameter, it needs to contain the desired machine, cpu |
| // and include -enable-kvm if necessary. |
| // "{{INDEX}}" is replaced with 0-based index of the VM (from 0 to Count-1). |
| // "{{TEMPLATE}}" is replaced with the path to a copy of workdir/template dir. |
| // "{{TCP_PORT}}" is replaced with a random free TCP port |
| // "{{FN%8}}" is replaced with PCI BDF (Function#%8) and PCI BDF Dev# += index/8 |
| QemuArgs string `json:"qemu_args"` |
| // Location of the kernel for injected boot (e.g. arch/x86/boot/bzImage, optional). |
| // This is passed to QEMU as the -kernel option. |
| Kernel string `json:"kernel"` |
| // Additional command line options for the booting kernel, for example `root=/dev/sda1`. |
| // Can only be specified with kernel. |
| Cmdline string `json:"cmdline"` |
| // Initial ramdisk, passed via -initrd QEMU flag (optional). |
| Initrd string `json:"initrd"` |
| // QEMU image device. |
| // The default value "hda" is transformed to "-hda image" for QEMU. |
| // The modern way of describing QEMU hard disks is supported, so the value |
| // "drive index=0,media=disk,file=" is transformed to "-drive index=0,media=disk,file=image" for QEMU. |
| ImageDevice string `json:"image_device"` |
| // EFI images containing the EFI itself, as well as this VMs EFI variables. |
| EfiCodeDevice string `json:"efi_code_device"` |
| EfiVarsDevice string `json:"efi_vars_device"` |
| // QEMU network device type to use. |
| // If not specified, some default per-arch value will be used. |
| // See the full list with qemu-system-x86_64 -device help. |
| NetDev string `json:"network_device"` |
| // Number of VM CPUs (1 by default). |
| CPU int `json:"cpu"` |
| // Amount of VM memory in MiB (1024 by default). |
| Mem int `json:"mem"` |
| // For building kernels without -snapshot for pkg/build (true by default). |
| Snapshot bool `json:"snapshot"` |
| // Magic key used to dongle macOS to the device. |
| AppleSmcOsk string `json:"apple_smc_osk"` |
| } |
| |
| type Pool struct { |
| env *vmimpl.Env |
| cfg *Config |
| target *targets.Target |
| archConfig *archConfig |
| version string |
| } |
| |
| type instance struct { |
| index int |
| cfg *Config |
| target *targets.Target |
| archConfig *archConfig |
| version string |
| args []string |
| image string |
| debug bool |
| os string |
| workdir string |
| sshkey string |
| sshuser string |
| timeouts targets.Timeouts |
| port int |
| monport int |
| forwardPort int |
| mon net.Conn |
| monEnc *json.Encoder |
| monDec *json.Decoder |
| rpipe io.ReadCloser |
| wpipe io.WriteCloser |
| qemu *exec.Cmd |
| merger *vmimpl.OutputMerger |
| files map[string]string |
| *snapshot |
| } |
| |
| type archConfig struct { |
| Qemu string |
| QemuArgs string |
| TargetDir string // "/" by default |
| NetDev string // default network device type (see the full list with qemu-system-x86_64 -device help) |
| RngDev string // default rng device (optional) |
| // UseNewQemuImageOptions specifies whether the arch uses "new" QEMU image device options. |
| UseNewQemuImageOptions bool |
| CmdLine []string |
| } |
| |
| var archConfigs = map[string]*archConfig{ |
| "linux/amd64": { |
| Qemu: "qemu-system-x86_64", |
| QemuArgs: "-enable-kvm -cpu host,migratable=off", |
| // e1000e fails on recent Debian distros with: |
| // Initialization of device e1000e failed: failed to find romfile "efi-e1000e.rom |
| // But other arches don't use e1000e, e.g. arm64 uses virtio by default. |
| NetDev: "e1000", |
| RngDev: "virtio-rng-pci", |
| CmdLine: []string{ |
| "root=/dev/sda", |
| "console=ttyS0", |
| }, |
| }, |
| "linux/386": { |
| Qemu: "qemu-system-i386", |
| NetDev: "e1000", |
| RngDev: "virtio-rng-pci", |
| CmdLine: []string{ |
| "root=/dev/sda", |
| "console=ttyS0", |
| }, |
| }, |
| "linux/arm64": { |
| Qemu: "qemu-system-aarch64", |
| // Disable SVE and pointer authentication for now, they significantly slow down |
| // the emulation and are unlikely to bring a lot of new coverage. |
| QemuArgs: strings.Join([]string{"-machine virt,virtualization=on,gic-version=max ", |
| "-cpu max,sve=off,pauth=off -accel tcg,thread=multi"}, ""), |
| NetDev: "virtio-net-pci", |
| RngDev: "virtio-rng-pci", |
| CmdLine: []string{ |
| "root=/dev/vda", |
| "console=ttyAMA0", |
| }, |
| }, |
| "linux/arm": { |
| Qemu: "qemu-system-arm", |
| QemuArgs: "-machine vexpress-a15 -cpu max -accel tcg,thread=multi", |
| NetDev: "virtio-net-device", |
| RngDev: "virtio-rng-device", |
| UseNewQemuImageOptions: true, |
| CmdLine: []string{ |
| "root=/dev/vda", |
| "console=ttyAMA0", |
| }, |
| }, |
| "linux/mips64le": { |
| Qemu: "qemu-system-mips64el", |
| QemuArgs: "-M malta -cpu MIPS64R2-generic -nodefaults", |
| NetDev: "e1000", |
| RngDev: "virtio-rng-pci", |
| CmdLine: []string{ |
| "root=/dev/sda", |
| "console=ttyS0", |
| }, |
| }, |
| "linux/ppc64le": { |
| Qemu: "qemu-system-ppc64", |
| QemuArgs: "-enable-kvm -vga none", |
| NetDev: "virtio-net-pci", |
| RngDev: "virtio-rng-pci", |
| }, |
| "linux/riscv64": { |
| Qemu: "qemu-system-riscv64", |
| QemuArgs: "-machine virt", |
| NetDev: "virtio-net-pci", |
| RngDev: "virtio-rng-pci", |
| UseNewQemuImageOptions: true, |
| CmdLine: []string{ |
| "root=/dev/vda", |
| "console=ttyS0", |
| }, |
| }, |
| "linux/s390x": { |
| Qemu: "qemu-system-s390x", |
| QemuArgs: "-M s390-ccw-virtio -cpu max,zpci=on", |
| NetDev: "virtio-net-pci", |
| RngDev: "virtio-rng-ccw", |
| CmdLine: []string{ |
| "root=/dev/vda", |
| // The following kernel parameters is a temporary |
| // work-around for not having CONFIG_CMDLINE on s390x. |
| "net.ifnames=0", |
| "biosdevname=0", |
| }, |
| }, |
| "freebsd/amd64": { |
| Qemu: "qemu-system-x86_64", |
| QemuArgs: "-enable-kvm", |
| NetDev: "e1000", |
| RngDev: "virtio-rng-pci", |
| }, |
| "freebsd/riscv64": { |
| Qemu: "qemu-system-riscv64", |
| QemuArgs: "-machine virt", |
| NetDev: "virtio-net-pci", |
| RngDev: "virtio-rng-pci", |
| UseNewQemuImageOptions: true, |
| }, |
| "darwin/amd64": { |
| Qemu: "qemu-system-x86_64", |
| QemuArgs: strings.Join([]string{ |
| "-accel hvf -machine q35 ", |
| "-cpu Penryn,vendor=GenuineIntel,+invtsc,vmware-cpuid-freq=on,", |
| "+pcid,+ssse3,+sse4.2,+popcnt,+avx,+aes,+xsave,+xsaveopt,check ", |
| }, ""), |
| TargetDir: "/tmp", |
| NetDev: "e1000-82545em", |
| RngDev: "virtio-rng-pci", |
| }, |
| "netbsd/amd64": { |
| Qemu: "qemu-system-x86_64", |
| QemuArgs: "-enable-kvm", |
| NetDev: "e1000", |
| RngDev: "virtio-rng-pci", |
| }, |
| "fuchsia/amd64": { |
| Qemu: "qemu-system-x86_64", |
| QemuArgs: "-enable-kvm -machine q35 -cpu host,migratable=off", |
| TargetDir: "/tmp", |
| NetDev: "e1000", |
| RngDev: "virtio-rng-pci", |
| CmdLine: []string{ |
| "kernel.serial=legacy", |
| "kernel.halt-on-panic=true", |
| // Set long (300sec) thresholds for kernel lockup detector to |
| // prevent false alarms from potentially oversubscribed hosts. |
| // (For more context, see fxbug.dev/109612.) |
| "kernel.lockup-detector.critical-section-threshold-ms=300000", |
| "kernel.lockup-detector.critical-section-fatal-threshold-ms=300000", |
| "kernel.lockup-detector.heartbeat-age-threshold-ms=300000", |
| "kernel.lockup-detector.heartbeat-age-fatal-threshold-ms=300000", |
| }, |
| }, |
| } |
| |
| func ctor(env *vmimpl.Env) (vmimpl.Pool, error) { |
| archConfig := archConfigs[env.OS+"/"+env.Arch] |
| cfg := &Config{ |
| Count: 1, |
| CPU: 1, |
| Mem: 1024, |
| ImageDevice: "hda", |
| Qemu: archConfig.Qemu, |
| QemuArgs: archConfig.QemuArgs, |
| NetDev: archConfig.NetDev, |
| Snapshot: true, |
| } |
| if err := config.LoadData(env.Config, cfg); err != nil { |
| return nil, fmt.Errorf("failed to parse qemu vm config: %w", err) |
| } |
| if cfg.Count < 1 || cfg.Count > 128 { |
| return nil, fmt.Errorf("invalid config param count: %v, want [1, 128]", cfg.Count) |
| } |
| if env.Debug && cfg.Count > 1 { |
| log.Logf(0, "limiting number of VMs from %v to 1 in debug mode", cfg.Count) |
| cfg.Count = 1 |
| } |
| if _, err := exec.LookPath(cfg.Qemu); err != nil { |
| return nil, err |
| } |
| if env.Image == "9p" { |
| if env.OS != targets.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 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) |
| |
| output, err := osutil.RunCmd(time.Minute, "", cfg.Qemu, "--version") |
| if err != nil { |
| return nil, err |
| } |
| version := string(bytes.Split(output, []byte{'\n'})[0]) |
| |
| pool := &Pool{ |
| env: env, |
| cfg: cfg, |
| version: version, |
| target: targets.Get(env.OS, env.Arch), |
| archConfig: archConfig, |
| } |
| 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: %w", 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 |
| } |
| if i < 1000 && strings.Contains(err.Error(), "Device or resource busy") { |
| continue |
| } |
| return nil, err |
| } |
| } |
| |
| func (pool *Pool) ctor(workdir, sshkey, sshuser string, index int) (*instance, error) { |
| inst := &instance{ |
| index: index, |
| cfg: pool.cfg, |
| target: pool.target, |
| archConfig: pool.archConfig, |
| version: pool.version, |
| image: pool.env.Image, |
| debug: pool.env.Debug, |
| os: pool.env.OS, |
| timeouts: pool.env.Timeouts, |
| workdir: workdir, |
| sshkey: sshkey, |
| sshuser: sshuser, |
| } |
| if pool.env.Snapshot { |
| inst.snapshot = new(snapshot) |
| } |
| if st, err := os.Stat(inst.image); err == nil && st.Size() == 0 { |
| // Some kernels may not need an image, however caller may still |
| // want to pass us a fake empty image because the rest of syzkaller |
| // assumes that an image is mandatory. So if the image is empty, we ignore it. |
| inst.image = "" |
| } |
| 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() error { |
| if inst.qemu != nil { |
| inst.qemu.Process.Kill() |
| inst.qemu.Wait() |
| } |
| if inst.merger != nil { |
| inst.merger.Wait() |
| } |
| if inst.rpipe != nil { |
| inst.rpipe.Close() |
| } |
| if inst.wpipe != nil { |
| inst.wpipe.Close() |
| } |
| if inst.mon != nil { |
| inst.mon.Close() |
| } |
| if inst.snapshot != nil { |
| inst.snapshotClose() |
| } |
| return nil |
| } |
| |
| func (inst *instance) boot() error { |
| inst.port = vmimpl.UnusedTCPPort() |
| inst.monport = vmimpl.UnusedTCPPort() |
| args, err := inst.buildQemuArgs() |
| if err != nil { |
| return err |
| } |
| if inst.debug { |
| log.Logf(0, "running command: %v %#v", inst.cfg.Qemu, args) |
| } |
| inst.args = 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: %w", 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 |
| } |
| } |
| }() |
| |
| if inst.snapshot != nil { |
| if err := inst.snapshotHandshake(); err != nil { |
| return err |
| } |
| } |
| |
| if err := vmimpl.WaitForSSH(inst.debug, 10*time.Minute*inst.timeouts.Scale, "localhost", |
| inst.sshkey, inst.sshuser, inst.os, inst.port, inst.merger.Err, false); err != nil { |
| bootOutputStop <- true |
| <-bootOutputStop |
| return vmimpl.MakeBootError(err, bootOutput) |
| } |
| bootOutputStop <- true |
| return nil |
| } |
| |
| func (inst *instance) buildQemuArgs() ([]string, error) { |
| args := []string{ |
| "-m", strconv.Itoa(inst.cfg.Mem), |
| "-smp", strconv.Itoa(inst.cfg.CPU), |
| "-chardev", fmt.Sprintf("socket,id=SOCKSYZ,server=on,wait=off,host=localhost,port=%v", inst.monport), |
| "-mon", "chardev=SOCKSYZ,mode=control", |
| "-display", "none", |
| "-serial", "stdio", |
| "-no-reboot", |
| "-name", fmt.Sprintf("VM-%v", inst.index), |
| } |
| if inst.archConfig.RngDev != "" { |
| args = append(args, "-device", inst.archConfig.RngDev) |
| } |
| templateDir := filepath.Join(inst.workdir, "template") |
| args = append(args, splitArgs(inst.cfg.QemuArgs, templateDir, inst.index)...) |
| args = append(args, |
| "-device", inst.cfg.NetDev+",netdev=net0", |
| "-netdev", fmt.Sprintf("user,id=net0,restrict=on,hostfwd=tcp:127.0.0.1:%v-:22", inst.port), |
| ) |
| 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 if inst.image != "" { |
| if inst.archConfig.UseNewQemuImageOptions { |
| args = append(args, |
| "-device", "virtio-blk-device,drive=hd0", |
| "-drive", fmt.Sprintf("file=%v,if=none,format=raw,id=hd0", inst.image), |
| ) |
| } else { |
| // inst.cfg.ImageDevice can contain spaces |
| imgline := strings.Split(inst.cfg.ImageDevice, " ") |
| imgline[0] = "-" + imgline[0] |
| if strings.HasSuffix(imgline[len(imgline)-1], "file=") { |
| imgline[len(imgline)-1] = imgline[len(imgline)-1] + inst.image |
| } else { |
| imgline = append(imgline, inst.image) |
| } |
| args = append(args, imgline...) |
| } |
| if inst.cfg.Snapshot { |
| args = append(args, "-snapshot") |
| } |
| } |
| if inst.cfg.Initrd != "" { |
| args = append(args, |
| "-initrd", inst.cfg.Initrd, |
| ) |
| } |
| if inst.cfg.Kernel != "" { |
| cmdline := append([]string{}, inst.archConfig.CmdLine...) |
| 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"), |
| ) |
| } |
| cmdline = append(cmdline, inst.cfg.Cmdline) |
| args = append(args, |
| "-kernel", inst.cfg.Kernel, |
| "-append", strings.Join(cmdline, " "), |
| ) |
| } |
| if inst.cfg.EfiCodeDevice != "" { |
| args = append(args, |
| "-drive", "if=pflash,format=raw,readonly=on,file="+inst.cfg.EfiCodeDevice, |
| ) |
| } |
| if inst.cfg.EfiVarsDevice != "" { |
| args = append(args, |
| "-drive", "if=pflash,format=raw,readonly=on,file="+inst.cfg.EfiVarsDevice, |
| ) |
| } |
| if inst.cfg.AppleSmcOsk != "" { |
| args = append(args, |
| "-device", "isa-applesmc,osk="+inst.cfg.AppleSmcOsk, |
| ) |
| } |
| if inst.snapshot != nil { |
| snapshotArgs, err := inst.snapshotEnable() |
| if err != nil { |
| return nil, err |
| } |
| args = append(args, snapshotArgs...) |
| } |
| return args, nil |
| } |
| |
| // "vfio-pci,host=BN:DN.{{FN%8}},addr=0x11". |
| func handleVfioPciArg(arg string, index int) string { |
| if !strings.Contains(arg, "{{FN%8}}") { |
| return arg |
| } |
| if index > 7 { |
| re := regexp.MustCompile(`vfio-pci,host=[a-bA-B0-9]+(:[a-bA-B0-9]{1,8}).{{FN%8}},[^:.,]+$`) |
| matches := re.FindAllStringSubmatch(arg, -1) |
| if len(matches[0]) != 2 { |
| return arg |
| } |
| submatch := matches[0][1] |
| dnSubmatch, _ := strconv.ParseInt(submatch[1:], 16, 64) |
| devno := dnSubmatch + int64(index/8) |
| arg = strings.ReplaceAll(arg, submatch, fmt.Sprintf(":%02x", devno)) |
| } |
| arg = strings.ReplaceAll(arg, "{{FN%8}}", fmt.Sprint(index%8)) |
| return arg |
| } |
| |
| func splitArgs(str, templateDir string, index int) (args []string) { |
| for _, arg := range strings.Split(str, " ") { |
| if arg == "" { |
| continue |
| } |
| arg = strings.ReplaceAll(arg, "{{INDEX}}", fmt.Sprint(index)) |
| arg = strings.ReplaceAll(arg, "{{TEMPLATE}}", templateDir) |
| arg = handleVfioPciArg(arg, index) |
| const tcpPort = "{{TCP_PORT}}" |
| if strings.Contains(arg, tcpPort) { |
| arg = strings.ReplaceAll(arg, tcpPort, fmt.Sprint(vmimpl.UnusedTCPPort())) |
| } |
| args = append(args, arg) |
| } |
| return |
| } |
| |
| func (inst *instance) Forward(port int) (string, error) { |
| if port == 0 { |
| return "", fmt.Errorf("vm/qemu: forward port is zero") |
| } |
| if !inst.target.HostFuzzer { |
| if inst.forwardPort != 0 { |
| return "", fmt.Errorf("vm/qemu: forward port already set") |
| } |
| inst.forwardPort = port |
| } |
| return fmt.Sprintf("localhost:%v", port), nil |
| } |
| |
| func (inst *instance) targetDir() string { |
| if inst.image == "9p" { |
| return "/tmp" |
| } |
| if inst.archConfig.TargetDir == "" { |
| return "/" |
| } |
| return inst.archConfig.TargetDir |
| } |
| |
| func (inst *instance) Copy(hostSrc string) (string, error) { |
| base := filepath.Base(hostSrc) |
| vmDst := filepath.Join(inst.targetDir(), base) |
| if inst.target.HostFuzzer { |
| if base == "syz-execprog" { |
| return hostSrc, nil // we will run these on host |
| } |
| if inst.files == nil { |
| inst.files = make(map[string]string) |
| } |
| inst.files[vmDst] = hostSrc |
| } |
| |
| args := append(vmimpl.SCPArgs(inst.debug, inst.sshkey, inst.port, false), |
| hostSrc, inst.sshuser+"@localhost:"+vmDst) |
| if inst.debug { |
| log.Logf(0, "running command: scp %#v", args) |
| } |
| _, err := osutil.RunCmd(10*time.Minute*inst.timeouts.Scale, "", "scp", args...) |
| 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) |
| |
| sshArgs := vmimpl.SSHArgsForward(inst.debug, inst.sshkey, inst.port, inst.forwardPort, false) |
| args := strings.Split(command, " ") |
| if bin := filepath.Base(args[0]); inst.target.HostFuzzer && bin == "syz-execprog" { |
| // Weird mode for Fuchsia. |
| // Fuzzer and execprog are on host (we did not copy them), so we will run them as is, |
| // but we will also wrap executor with ssh invocation. |
| for i, arg := range args { |
| if strings.HasPrefix(arg, "-executor=") { |
| args[i] = "-executor=" + "/usr/bin/ssh " + strings.Join(sshArgs, " ") + |
| " " + inst.sshuser + "@localhost " + arg[len("-executor="):] |
| } |
| if host := inst.files[arg]; host != "" { |
| args[i] = host |
| } |
| } |
| } else { |
| args = []string{"ssh"} |
| args = append(args, sshArgs...) |
| args = append(args, inst.sshuser+"@localhost", "cd "+inst.targetDir()+" && "+command) |
| } |
| if inst.debug { |
| log.Logf(0, "running command: %#v", args) |
| } |
| cmd := osutil.Command(args[0], args[1:]...) |
| cmd.Dir = inst.workdir |
| cmd.Stdout = wpipe |
| cmd.Stderr = wpipe |
| if err := cmd.Start(); err != nil { |
| wpipe.Close() |
| return nil, nil, err |
| } |
| wpipe.Close() |
| return vmimpl.Multiplex(cmd, inst.merger, timeout, vmimpl.MultiplexConfig{ |
| Stop: stop, |
| Debug: inst.debug, |
| Scale: inst.timeouts.Scale, |
| }) |
| } |
| |
| func (inst *instance) Info() ([]byte, error) { |
| info := fmt.Sprintf("%v\n%v %q\n", inst.version, inst.cfg.Qemu, inst.args) |
| return []byte(info), nil |
| } |
| |
| func (inst *instance) Diagnose(rep *report.Report) ([]byte, bool) { |
| if inst.target.OS == targets.Linux { |
| if output, wait, handled := vmimpl.DiagnoseLinux(rep, inst.ssh); handled { |
| return output, wait |
| } |
| } |
| // TODO: we don't need registers on all reports. Probably only relevant for "crashes" |
| // (NULL derefs, paging faults, etc), but is not useful for WARNING/BUG/HANG (?). |
| ret := []byte(fmt.Sprintf("%s Registers:\n", time.Now().Format("15:04:05 "))) |
| for cpu := 0; cpu < inst.cfg.CPU; cpu++ { |
| regs, err := inst.hmp("info registers", cpu) |
| if err == nil { |
| ret = append(ret, []byte(fmt.Sprintf("info registers vcpu %v\n", cpu))...) |
| ret = append(ret, []byte(regs)...) |
| } else { |
| log.Logf(0, "VM-%v failed reading regs: %v", inst.index, err) |
| ret = append(ret, []byte(fmt.Sprintf("Failed reading regs: %v\n", err))...) |
| } |
| } |
| return ret, false |
| } |
| |
| func (inst *instance) ssh(args ...string) ([]byte, error) { |
| return osutil.RunCmd(time.Minute*inst.timeouts.Scale, "", "ssh", inst.sshArgs(args...)...) |
| } |
| |
| func (inst *instance) sshArgs(args ...string) []string { |
| sshArgs := append(vmimpl.SSHArgs(inst.debug, inst.sshkey, inst.port, false), inst.sshuser+"@localhost") |
| return append(sshArgs, args...) |
| } |
| |
| // nolint: lll |
| 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 |
| mkdir -p /etc/network/if-pre-up.d |
| mkdir -p /etc/network/if-up.d |
| ifup lo |
| ifup eth0 || true |
| 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 |
| groupadd -g 33 sshd |
| useradd -u 33 -g 33 -c sshd -d / 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 |
| ` |