| // Copyright 2020 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package fuzz |
| |
| import ( |
| "bufio" |
| "bytes" |
| "context" |
| "crypto/rand" |
| "encoding/hex" |
| "errors" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "net" |
| "os" |
| "os/exec" |
| "path" |
| "path/filepath" |
| "strconv" |
| "syscall" |
| "time" |
| |
| "github.com/golang/glog" |
| "go.fuchsia.dev/fuchsia/tools/qemu" |
| ) |
| |
| // A Launcher manages the lifecycle of an instance; e.g. starting and stopping |
| type Launcher interface { |
| // Do any preparation necessary before starting. This is idempotent, and |
| // does not need to be explicitly called by the client, as it will be |
| // automatically called by Start as necessary. |
| Prepare() error |
| |
| // Starts the instance, returning a Connector that can be used to communicate with it |
| Start() (Connector, error) |
| |
| // Returns true iff the instance is running. |
| IsRunning() (bool, error) |
| |
| // Stops the instance. This is allowed to take up to 3 seconds to return. |
| Kill() error |
| |
| // Dump any available system or debug logs to `out` |
| GetLogs(out io.Writer) error |
| } |
| |
| const successfulBootMarker = "{{{reset}}}" |
| const qemuBootTimeout = 10 * time.Second |
| |
| // A QemuLauncher starts Fuchsia on QEMU |
| type QemuLauncher struct { |
| Pid int |
| TmpDir string |
| |
| build Build |
| |
| // Paths to files that are created by Prepare(): |
| initrd string |
| extendedBlk string |
| sshKey string |
| sshPublicKey string |
| |
| // Overridable for testing |
| timeout time.Duration |
| } |
| |
| // qemuConfig contains all the configuration parameters that need to be passed |
| // from the Launcher to the qemu invocation |
| type qemuConfig struct { |
| binary string |
| kernel string |
| initrd string |
| blk string |
| logFile string |
| port int |
| } |
| |
| // NewQemuLauncher constructs a new QemuLauncher |
| func NewQemuLauncher(build Build) *QemuLauncher { |
| return &QemuLauncher{build: build, timeout: qemuBootTimeout} |
| } |
| |
| // Find an unused TCP port on the host |
| func getFreePort() (int, error) { |
| listener, err := net.Listen("tcp", "localhost:0") |
| if err != nil { |
| return 0, err |
| } |
| |
| port := listener.Addr().(*net.TCPAddr).Port |
| |
| // Technically this might get used before QEMU starts... |
| listener.Close() |
| return port, nil |
| } |
| |
| // Configure QEMU appropriately |
| func getQemuInvocation(config qemuConfig) ([]string, error) { |
| qemuCmd := &qemu.QEMUCommandBuilder{} |
| |
| qemuCmd.SetBinary(config.binary) |
| qemuCmd.SetTarget(qemu.TargetEnum.X86_64, true /* KVM */) |
| qemuCmd.SetKernel(config.kernel) |
| qemuCmd.SetInitrd(config.initrd) |
| |
| qemuCmd.SetCPUCount(4) |
| qemuCmd.SetMemory(3072 /* MiB */) |
| |
| qemuCmd.AddVirtioBlkPciDrive(qemu.Drive{ |
| ID: "d0", |
| File: config.blk, |
| }) |
| |
| network := qemu.Netdev{ |
| ID: "net0", |
| Device: qemu.Device{Model: qemu.DeviceModelVirtioNetPCI}, |
| User: &qemu.NetdevUser{ |
| Network: "192.168.3.0/24", |
| DHCPStart: "192.168.3.9", |
| Host: "192.168.3.2", |
| Forwards: []qemu.Forward{{HostPort: config.port, GuestPort: 22}}, |
| }, |
| } |
| network.Device.AddOption("mac", "52:54:00:63:5e:7b") |
| qemuCmd.AddNetwork(network) |
| |
| // The system will halt on a kernel panic instead of rebooting. |
| qemuCmd.AddKernelArg("kernel.halt-on-panic=true") |
| // Do not print colors. |
| qemuCmd.AddKernelArg("TERM=dumb") |
| // Necessary to redirect serial to stdout for x86. |
| qemuCmd.AddKernelArg("kernel.serial=legacy") |
| |
| qemuCmd.SetFlag("-nographic") |
| qemuCmd.SetFlag("-monitor", "none") |
| |
| // Disable kernel lockup detector in emulated environments to prevent false alarms from |
| // potentially oversubscribed hosts. (fxbug.dev/92109) |
| qemuCmd.AddKernelArg("kernel.lockup-detector.critical-section-threshold-ms=0") |
| qemuCmd.AddKernelArg("kernel.lockup-detector.critical-section-fatal-threshold-ms=0") |
| qemuCmd.AddKernelArg("kernel.lockup-detector.heartbeat-period-ms=0") |
| qemuCmd.AddKernelArg("kernel.lockup-detector.heartbeat-age-threshold-ms=0") |
| qemuCmd.AddKernelArg("kernel.lockup-detector.heartbeat-age-fatal-threshold-ms=0") |
| |
| // Disable the virtcon. |
| qemuCmd.AddKernelArg("virtcon.disable=true") |
| |
| // Unbuffer log output. |
| qemuCmd.AddKernelArg("kernel.bypass-debuglog=true") |
| |
| // Send serial to a log file. We don't want to attach directly via stdout, |
| // because the QEMU process needs to be able outlive this one. |
| qemuCmd.SetFlag("-serial", "file:"+config.logFile) |
| |
| entropy := make([]byte, 32) |
| if _, err := rand.Read(entropy); err == nil { |
| qemuCmd.AddKernelArg("kernel.entropy-mixin=" + hex.EncodeToString(entropy)) |
| } |
| |
| return qemuCmd.Build() |
| } |
| |
| func fileExists(path string) bool { |
| _, err := os.Stat(path) |
| return !os.IsNotExist(err) |
| } |
| |
| // Prepare files that are needed by QEMU, if they haven't already been prepared. |
| // |
| // If Prepare succeeds, it is up to the caller to clean up by calling Kill later. |
| // However, if it fails, any resources will have been automatically released. |
| func (q *QemuLauncher) Prepare() (returnErr error) { |
| paths, err := q.build.Path("zbi", "fvm", "blk", "zbitool") |
| if err != nil { |
| return fmt.Errorf("Error resolving qemu dependencies: %s", err) |
| } |
| zbi, fvm, blk, zbitool := paths[0], paths[1], paths[2], paths[3] |
| |
| // Create a tmpdir to store files we need |
| if q.TmpDir == "" { |
| tmpDir, err := ioutil.TempDir("", "clusterfuchsia-qemu-") |
| if err != nil { |
| return fmt.Errorf("Error creating tempdir: %s", err) |
| } |
| q.TmpDir = tmpDir |
| } |
| |
| // If we fail after this point, we need to make sure to clean up |
| defer func() { |
| if returnErr != nil { |
| q.cleanup() |
| } |
| }() |
| |
| sshKey := filepath.Join(q.TmpDir, "sshid") |
| sshPublicKey := filepath.Join(q.TmpDir, "sshid.pub") |
| initrd := filepath.Join(q.TmpDir, "ssh-"+path.Base(zbi)) |
| if !fileExists(sshKey) || !fileExists(sshPublicKey) { |
| // Generate SSH key pair |
| key, err := createSSHKey() |
| if err != nil { |
| return fmt.Errorf("error generating ssh key: %s", err) |
| } |
| if err := writeSSHPrivateKeyFile(key, sshKey); err != nil { |
| return fmt.Errorf("error writing ssh key: %s", err) |
| } |
| if err := writeSSHPublicKeyFile(key, sshPublicKey); err != nil { |
| return fmt.Errorf("error writing ssh public key: %s", err) |
| } |
| // Force rebuild of any existing ZBI in the next step, to reflect the new key |
| if err := os.RemoveAll(initrd); err != nil { |
| return fmt.Errorf("error removing zbi: %s", err) |
| } |
| } |
| q.sshKey = sshKey |
| q.sshPublicKey = sshPublicKey |
| |
| // Stick our SSH key into the authorized_keys files |
| if !fileExists(initrd) { |
| entry := "data/ssh/authorized_keys=" + q.sshPublicKey |
| if err := CreateProcessForeground(zbitool, "-o", initrd, zbi, "-e", entry); err != nil { |
| return fmt.Errorf("adding ssh key failed: %s", err) |
| } |
| } |
| q.initrd = initrd |
| |
| // Make an expanded copy of the disk image |
| extendedBlk := path.Join(q.TmpDir, "extended-"+path.Base(blk)) |
| if !fileExists(extendedBlk) { |
| if err := CreateProcessForeground("cp", blk, extendedBlk); err != nil { |
| return fmt.Errorf("cp failed: %s", err) |
| } |
| if err := CreateProcessForeground(fvm, extendedBlk, "extend", |
| "--length", "3G"); err != nil { |
| return fmt.Errorf("fvm failed: %s", err) |
| } |
| } |
| q.extendedBlk = extendedBlk |
| |
| return nil |
| } |
| |
| // Start launches QEMU and waits for it to get through the basic boot sequence. |
| // Note that networking will not necessarily be fully up by the time Start() returns |
| // |
| // If Start succeeds, it is up to the caller to clean up by calling Kill later. |
| // However, if it fails, any resources will have been automatically released. |
| func (q *QemuLauncher) Start() (conn Connector, returnErr error) { |
| running, err := q.IsRunning() |
| if err != nil { |
| return nil, fmt.Errorf("Error checking run state: %s", err) |
| } |
| |
| if running { |
| return nil, fmt.Errorf("Start called but already running") |
| } |
| |
| paths, err := q.build.Path("qemu", "kernel") |
| if err != nil { |
| return nil, fmt.Errorf("Error resolving qemu dependencies: %s", err) |
| } |
| binary, kernel := paths[0], paths[1] |
| |
| port, err := getFreePort() |
| if err != nil { |
| return nil, fmt.Errorf("couldn't get free port: %s", err) |
| } |
| |
| if err := q.Prepare(); err != nil { |
| return nil, fmt.Errorf("error while preparing to start: %s", err) |
| } |
| |
| // If we fail after this point, we need to make sure to clean up |
| defer func() { |
| if returnErr != nil { |
| q.cleanup() |
| } |
| }() |
| |
| invocation, err := getQemuInvocation(qemuConfig{ |
| binary: binary, |
| kernel: kernel, |
| initrd: q.initrd, |
| blk: q.extendedBlk, |
| logFile: q.logPath(), |
| port: port}) |
| if err != nil { |
| return nil, fmt.Errorf("qemu configuration error: %s", err) |
| } |
| |
| cmd := NewCommand(invocation[0], invocation[1:]...) |
| |
| logFile, err := os.Create(q.logPath()) |
| if err != nil { |
| return nil, fmt.Errorf("error creating qemu log: %s", err) |
| } |
| defer logFile.Close() |
| |
| // Save output from QEMU itself in case of error. This should be very |
| // minimal in size, if present at all. |
| // |
| // Watching for EOF on the pipe also doubles as a way of detecting |
| // subprocess exit without having to call Wait(), which we can't do since |
| // we intend to detach later. |
| var stdoutBuf bytes.Buffer |
| |
| outPipe, err := cmd.StdoutPipe() |
| if err != nil { |
| return nil, fmt.Errorf("error attaching stdout: %s", err) |
| } |
| cmd.Stderr = cmd.Stdout // capture stderr too |
| |
| // Use buffered channels, so goroutines will not block writing to them even |
| // if we never end up needing the result (i.e. because an error/success |
| // condition was reported sooner by another goroutine). |
| stdoutErrCh := make(chan error, 1) |
| go func() { |
| // Copy() will block until either the subprocess exits or we close the |
| // pipe before detaching. |
| if _, err := io.Copy(&stdoutBuf, outPipe); err != nil { |
| stdoutErrCh <- fmt.Errorf("error copying stdout: %s", err) |
| } |
| stdoutErrCh <- fmt.Errorf("qemu exited early") |
| }() |
| |
| if err := cmd.Start(); err != nil { |
| return nil, fmt.Errorf("failed to start qemu: %s", err) |
| } |
| |
| // Storage for system boot log, i.e. early serial output. |
| var logBuf bytes.Buffer |
| logBuf.Grow(64 * 1024) // Preallocate 64KB, which should be plenty. |
| |
| // Since we ignore EOFs when monitoring the QEMU serial log, the only |
| // straightforward way the goroutine below can know to exit is to be |
| // notified externally. This is done with a Context, which will either |
| // time out or be cancelled by the defer when this method returns. |
| ctx, cancel := context.WithTimeout(context.Background(), q.timeout) |
| defer cancel() |
| |
| // In the case of a successful boot, a nil error will be sent to the channel. |
| bootErrCh := make(chan error, 1) |
| go func() { |
| logReader := bufio.NewReader(logFile) |
| |
| // Tail the QEMU log file |
| for { |
| // Check for any early-exit conditions |
| select { |
| case <-ctx.Done(): |
| // The context.Context interface specifies that the |
| // returned error will be either DeadlineExceeded or |
| // Canceled. We should exit in both cases, but want to |
| // report an error in the timeout case. |
| if errors.Is(ctx.Err(), context.DeadlineExceeded) { |
| bootErrCh <- fmt.Errorf("timeout waiting for boot") |
| } |
| return |
| default: |
| } |
| |
| // See if any input is available |
| if _, err = logReader.Peek(1); err == io.EOF { |
| time.Sleep(100 * time.Millisecond) |
| continue |
| } else if err != nil { |
| bootErrCh <- fmt.Errorf("log peek failed: %s", err) |
| return |
| } |
| |
| if _, err := logBuf.ReadFrom(logReader); err != nil { |
| bootErrCh <- fmt.Errorf("log read failed: %s", err) |
| return |
| } |
| |
| // This is not as performant as something like a Scanner, but those |
| // can't recover from EOFs and it doesn't seem worth reimplementing. |
| if bytes.Contains(logBuf.Bytes(), []byte(successfulBootMarker)) { |
| // Early boot has finished |
| bootErrCh <- nil |
| return |
| } |
| } |
| }() |
| |
| glog.Info("Waiting for boot to complete...") |
| |
| var bootErr error |
| select { |
| case err := <-stdoutErrCh: |
| bootErr = fmt.Errorf("error starting qemu: %s", err) |
| case err := <-bootErrCh: |
| if err != nil { |
| bootErr = fmt.Errorf("error during boot: %s", err) |
| } |
| } |
| |
| if bootErr != nil { |
| // Kill the process, just in case this was an error other than early exit. |
| cmd.Process.Kill() |
| |
| // Dump logs before calling Wait(), which will release resources. |
| glog.Errorf("QEMU output: %s", stdoutBuf.String()) |
| glog.Errorf("Boot log: %s", logBuf.String()) |
| |
| // Wait on the process to prevent it from becoming a zombie. Errors |
| // don't matter here. |
| cmd.Wait() |
| |
| return nil, bootErr |
| } |
| |
| glog.Info("Instance started.") |
| q.Pid = cmd.Process.Pid |
| |
| // Detach from the child, since we will never wait on it. |
| outPipe.Close() |
| cmd.Process.Release() |
| |
| return NewSSHConnector("localhost", port, q.sshKey), nil |
| } |
| |
| // IsRunning checks if the qemu process is alive |
| func (q *QemuLauncher) IsRunning() (bool, error) { |
| if q.Pid == 0 { |
| return false, nil |
| } |
| |
| if err := CreateProcess("ps", "-p", strconv.Itoa(q.Pid)); err != nil { |
| if cmderr, ok := err.(*exec.ExitError); ok && cmderr.ExitCode() == 1 { |
| return false, nil |
| } |
| return false, fmt.Errorf("error running ps: %s", err) |
| } |
| |
| return true, nil |
| } |
| |
| // Cleans up any temporary files used by the launcher |
| func (q *QemuLauncher) cleanup() { |
| if q.TmpDir != "" { |
| if err := os.RemoveAll(q.TmpDir); err != nil { |
| glog.Warningf("failed to remove temp dir: %s", err) |
| } |
| q.TmpDir = "" |
| } |
| } |
| |
| // Kill tells the QEMU process to terminate, and cleans up the TmpDir |
| func (q *QemuLauncher) Kill() error { |
| if q.Pid != 0 { |
| glog.Infof("Killing PID %d", q.Pid) |
| |
| // TODO(fxbug.dev/45431): More gracefully, with timeout |
| if err := syscall.Kill(q.Pid, syscall.SIGKILL); err != nil { |
| glog.Warningf("failed to kill instance: %s", err) |
| } |
| |
| q.Pid = 0 |
| } |
| |
| q.cleanup() |
| |
| return nil |
| } |
| |
| func (q *QemuLauncher) logPath() string { |
| return filepath.Join(q.TmpDir, "qemu.log") |
| } |
| |
| // GetLogs writes any system logs from QEMU to `out` |
| func (q *QemuLauncher) GetLogs(out io.Writer) error { |
| f, err := os.Open(q.logPath()) |
| if err != nil { |
| return fmt.Errorf("error opening file: %s", err) |
| } |
| defer f.Close() |
| |
| if _, err := io.Copy(out, f); err != nil { |
| return fmt.Errorf("error while dumping log file %q: %s", q.logPath(), err) |
| } |
| |
| return nil |
| } |
| |
| func loadLauncherFromHandle(build Build, handle Handle) (Launcher, error) { |
| handleData, err := handle.GetData() |
| if err != nil { |
| return nil, err |
| } |
| |
| // Check that the Launcher is in a valid state |
| switch launcher := handleData.launcher.(type) { |
| case *QemuLauncher: |
| if launcher.Pid == 0 { |
| return nil, fmt.Errorf("pid not found in handle") |
| } |
| |
| if launcher.TmpDir == "" { |
| return nil, fmt.Errorf("tmpdir not found in handle") |
| } |
| |
| launcher.build = build |
| return launcher, nil |
| default: |
| return nil, fmt.Errorf("unknown launcher type: %T", handleData.launcher) |
| } |
| } |