aboutsummaryrefslogtreecommitdiffstats
path: root/vm/vmimpl
diff options
context:
space:
mode:
authorDmitry Vyukov <dvyukov@google.com>2017-06-02 20:09:00 +0200
committerDmitry Vyukov <dvyukov@google.com>2017-06-03 11:31:42 +0200
commitaf643baa328ae3d4b7076054bba648c4b8bf8056 (patch)
tree6e4687c745b63352dec21f6ac2a6a7d8fa1201c4 /vm/vmimpl
parent96b8d4e99c7812f91633ea6cd1aee5867965e742 (diff)
vm: overhaul
VM infrastructure currently has several problems: - Config struct is complete mess with a superset of params for all VM types - verification of Config is mess spread across several places - there is no place where VM code could do global initialization like creating GCE connection, uploading GCE image to GCS, matching adb devices with consoles, etc - it hard to add private VM implementations such impl would need to add code to config package which would lead to constant merge conflicts - interface for VM implementation is mixed with interface for VM users this does not allow to provide best interface for both of them - there is no way to add common code for all VM implementations This change solves these problems by: - splitting VM interface for users (vm package) and VM interface for VM implementations (vmimpl pacakge), this in turn allows to add common code - adding Pool concept that allows to do global initialization and config checking at the right time - decoupling manager config from VM-specific config each VM type now defines own config Note: manager configs need to be changed after this change: VM-specific parts are moved to own "vm" subobject. Note: this change also drops "local" VM type. Its story was long unclear and there is now syz-stress which solves the same problem.
Diffstat (limited to 'vm/vmimpl')
-rw-r--r--vm/vmimpl/console.go134
-rw-r--r--vm/vmimpl/merger.go81
-rw-r--r--vm/vmimpl/merger_test.go79
-rw-r--r--vm/vmimpl/util.go19
-rw-r--r--vm/vmimpl/vmimpl.go77
5 files changed, 390 insertions, 0 deletions
diff --git a/vm/vmimpl/console.go b/vm/vmimpl/console.go
new file mode 100644
index 000000000..f4a0b71d1
--- /dev/null
+++ b/vm/vmimpl/console.go
@@ -0,0 +1,134 @@
+// Copyright 2017 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.
+
+// +build !ppc64le
+
+package vmimpl
+
+import (
+ "fmt"
+ "io"
+ "os/exec"
+ "sync"
+ "syscall"
+ "unsafe"
+
+ "github.com/google/syzkaller/pkg/osutil"
+ "golang.org/x/sys/unix"
+)
+
+// Tested on Suzy-Q and BeagleBone.
+func OpenConsole(con string) (rc io.ReadCloser, err error) {
+ fd, err := syscall.Open(con, syscall.O_RDONLY|syscall.O_NOCTTY|syscall.O_SYNC, 0)
+ if err != nil {
+ return nil, fmt.Errorf("failed to open console file: %v", err)
+ }
+ defer func() {
+ if fd != -1 {
+ syscall.Close(fd)
+ }
+ }()
+ var term unix.Termios
+ if _, _, errno := syscall.Syscall(unix.SYS_IOCTL, uintptr(fd), unix.TCGETS2, uintptr(unsafe.Pointer(&term))); errno != 0 {
+ return nil, fmt.Errorf("failed to get console termios: %v", errno)
+ }
+ // no parity bit, only need 1 stop bit, no hardware flowcontrol
+ term.Cflag &^= unix.CBAUD | unix.CSIZE | unix.PARENB | unix.CSTOPB | unix.CRTSCTS
+ // ignore modem controls
+ term.Cflag |= unix.B115200 | unix.CS8 | unix.CLOCAL | unix.CREAD
+ // setup for non-canonical mode
+ term.Iflag &^= unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP | unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON
+ term.Lflag &^= unix.ECHO | unix.ECHONL | unix.ICANON | unix.ISIG | unix.IEXTEN
+ term.Oflag &^= unix.OPOST
+ term.Cc[unix.VMIN] = 0
+ term.Cc[unix.VTIME] = 10 // 1 second timeout
+ if _, _, errno := syscall.Syscall(unix.SYS_IOCTL, uintptr(fd), unix.TCSETS2, uintptr(unsafe.Pointer(&term))); errno != 0 {
+ return nil, fmt.Errorf("failed to get console termios: %v", errno)
+ }
+ tmp := fd
+ fd = -1
+ return &tty{fd: tmp}, nil
+}
+
+type tty struct {
+ mu sync.Mutex
+ fd int
+}
+
+func (t *tty) Read(buf []byte) (int, error) {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+ if t.fd == -1 {
+ return 0, io.EOF
+ }
+ n, err := syscall.Read(t.fd, buf)
+ if n < 0 {
+ n = 0
+ }
+ return n, err
+}
+
+func (t *tty) Close() error {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+ if t.fd != -1 {
+ syscall.Close(t.fd)
+ t.fd = -1
+ }
+ return nil
+}
+
+// OpenAdbConsole provides fallback console output using 'adb shell dmesg -w'.
+func OpenAdbConsole(bin, dev string) (rc io.ReadCloser, err error) {
+ rpipe, wpipe, err := osutil.LongPipe()
+ if err != nil {
+ return nil, err
+ }
+ cmd := exec.Command(bin, "-s", dev, "shell", "dmesg -w")
+ cmd.Stdout = wpipe
+ cmd.Stderr = wpipe
+ if err := cmd.Start(); err != nil {
+ rpipe.Close()
+ wpipe.Close()
+ return nil, fmt.Errorf("failed to start adb: %v", err)
+ }
+ wpipe.Close()
+ con := &adbCon{
+ cmd: cmd,
+ rpipe: rpipe,
+ }
+ return con, err
+}
+
+type adbCon struct {
+ closeMu sync.Mutex
+ readMu sync.Mutex
+ cmd *exec.Cmd
+ rpipe io.ReadCloser
+}
+
+func (t *adbCon) Read(buf []byte) (int, error) {
+ t.readMu.Lock()
+ n, err := t.rpipe.Read(buf)
+ t.readMu.Unlock()
+ return n, err
+}
+
+func (t *adbCon) Close() error {
+ t.closeMu.Lock()
+ cmd := t.cmd
+ t.cmd = nil
+ t.closeMu.Unlock()
+ if cmd == nil {
+ return nil
+ }
+
+ cmd.Process.Kill()
+
+ t.readMu.Lock()
+ t.rpipe.Close()
+ t.readMu.Unlock()
+
+ cmd.Process.Wait()
+ return nil
+}
diff --git a/vm/vmimpl/merger.go b/vm/vmimpl/merger.go
new file mode 100644
index 000000000..17b837602
--- /dev/null
+++ b/vm/vmimpl/merger.go
@@ -0,0 +1,81 @@
+// Copyright 2016 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 vmimpl
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "sync"
+)
+
+type OutputMerger struct {
+ Output chan []byte
+ Err chan error
+ teeMu sync.Mutex
+ tee io.Writer
+ wg sync.WaitGroup
+}
+
+func NewOutputMerger(tee io.Writer) *OutputMerger {
+ return &OutputMerger{
+ Output: make(chan []byte, 1000),
+ Err: make(chan error, 1),
+ tee: tee,
+ }
+}
+
+func (merger *OutputMerger) Wait() {
+ merger.wg.Wait()
+ close(merger.Output)
+}
+
+func (merger *OutputMerger) Add(name string, r io.ReadCloser) {
+ merger.wg.Add(1)
+ go func() {
+ var pending []byte
+ var buf [4 << 10]byte
+ for {
+ n, err := r.Read(buf[:])
+ if n != 0 {
+ pending = append(pending, buf[:n]...)
+ if pos := bytes.LastIndexByte(pending, '\n'); pos != -1 {
+ out := pending[:pos+1]
+ if merger.tee != nil {
+ merger.teeMu.Lock()
+ merger.tee.Write(out)
+ merger.teeMu.Unlock()
+ }
+ select {
+ case merger.Output <- append([]byte{}, out...):
+ r := copy(pending[:], pending[pos+1:])
+ pending = pending[:r]
+ default:
+ }
+ }
+ }
+ if err != nil {
+ if len(pending) != 0 {
+ pending = append(pending, '\n')
+ if merger.tee != nil {
+ merger.teeMu.Lock()
+ merger.tee.Write(pending)
+ merger.teeMu.Unlock()
+ }
+ select {
+ case merger.Output <- pending:
+ default:
+ }
+ }
+ r.Close()
+ select {
+ case merger.Err <- fmt.Errorf("failed to read from %v: %v", name, err):
+ default:
+ }
+ merger.wg.Done()
+ return
+ }
+ }
+ }()
+}
diff --git a/vm/vmimpl/merger_test.go b/vm/vmimpl/merger_test.go
new file mode 100644
index 000000000..335dcb228
--- /dev/null
+++ b/vm/vmimpl/merger_test.go
@@ -0,0 +1,79 @@
+// Copyright 2016 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 vmimpl
+
+import (
+ "bytes"
+ "testing"
+ "time"
+
+ "github.com/google/syzkaller/pkg/osutil"
+)
+
+func TestMerger(t *testing.T) {
+ tee := new(bytes.Buffer)
+ merger := NewOutputMerger(tee)
+
+ rp1, wp1, err := osutil.LongPipe()
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer wp1.Close()
+ merger.Add("pipe1", rp1)
+
+ rp2, wp2, err := osutil.LongPipe()
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer wp2.Close()
+ merger.Add("pipe2", rp2)
+
+ wp1.Write([]byte("111"))
+ select {
+ case <-merger.Output:
+ t.Fatalf("merger produced incomplete line")
+ case <-time.After(10 * time.Millisecond):
+ }
+
+ wp2.Write([]byte("222"))
+ select {
+ case <-merger.Output:
+ t.Fatalf("merger produced incomplete line")
+ case <-time.After(10 * time.Millisecond):
+ }
+
+ wp1.Write([]byte("333\n444"))
+ got := string(<-merger.Output)
+ if want := "111333\n"; got != want {
+ t.Fatalf("bad line: '%s', want '%s'", got, want)
+ }
+
+ wp2.Write([]byte("555\n666\n777"))
+ got = string(<-merger.Output)
+ if want := "222555\n666\n"; got != want {
+ t.Fatalf("bad line: '%s', want '%s'", got, want)
+ }
+
+ wp1.Close()
+ got = string(<-merger.Output)
+ if want := "444\n"; got != want {
+ t.Fatalf("bad line: '%s', want '%s'", got, want)
+ }
+
+ if err := <-merger.Err; err == nil || err.Error() != "failed to read from pipe1: EOF" {
+ t.Fatalf("merger did not produce io.EOF: %v", err)
+ }
+
+ wp2.Close()
+ got = string(<-merger.Output)
+ if want := "777\n"; got != want {
+ t.Fatalf("bad line: '%s', want '%s'", got, want)
+ }
+
+ merger.Wait()
+ want := "111333\n222555\n666\n444\n777\n"
+ if got := string(tee.Bytes()); got != want {
+ t.Fatalf("bad tee: '%s', want '%s'", got, want)
+ }
+}
diff --git a/vm/vmimpl/util.go b/vm/vmimpl/util.go
new file mode 100644
index 000000000..f80de8ab1
--- /dev/null
+++ b/vm/vmimpl/util.go
@@ -0,0 +1,19 @@
+// Copyright 2017 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 vmimpl
+
+import (
+ "time"
+)
+
+// Sleep for d.
+// If shutdown is in progress, return false prematurely.
+func SleepInterruptible(d time.Duration) bool {
+ select {
+ case <-time.After(d):
+ return true
+ case <-Shutdown:
+ return false
+ }
+}
diff --git a/vm/vmimpl/vmimpl.go b/vm/vmimpl/vmimpl.go
new file mode 100644
index 000000000..4c542429c
--- /dev/null
+++ b/vm/vmimpl/vmimpl.go
@@ -0,0 +1,77 @@
+// Copyright 2017 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 vmimpl provides an abstract test machine (VM, physical machine, etc)
+// interface for the rest of the system. For convenience test machines are subsequently
+// collectively called VMs.
+// The package also provides various utility functions for VM implementations.
+package vmimpl
+
+import (
+ "errors"
+ "fmt"
+ "time"
+)
+
+// Pool represents a set of test machines (VMs, physical devices, etc) of particular type.
+type Pool interface {
+ // Count returns total number of VMs in the pool.
+ Count() int
+
+ // Create creates and boots a new VM instance.
+ Create(workdir string, index int) (Instance, error)
+}
+
+// Instance represents a single VM.
+type Instance interface {
+ // Copy copies a hostSrc file into VM and returns file name in VM.
+ Copy(hostSrc string) (string, error)
+
+ // Forward setups forwarding from within VM to host port port
+ // and returns address to use in VM.
+ Forward(port int) (string, error)
+
+ // Run runs cmd inside of the VM (think of ssh cmd).
+ // outc receives combined cmd and kernel console output.
+ // errc receives either command Wait return error or vmimpl.TimeoutErr.
+ // Command is terminated after timeout. Send on the stop chan can be used to terminate it earlier.
+ Run(timeout time.Duration, stop <-chan bool, command string) (outc <-chan []byte, errc <-chan error, err error)
+
+ // Close stops and destroys the VM.
+ Close()
+}
+
+// Env contains global constant parameters for a pool of VMs.
+type Env struct {
+ // Unique name
+ // Can be used for VM name collision resolution if several pools share global name space.
+ Name string
+ Workdir string
+ Image string
+ Debug bool
+ Config []byte // json-serialized VM-type-specific config
+}
+
+// Create creates a VM type that can be used to create individual VMs.
+func Create(typ string, env *Env) (Pool, error) {
+ ctor := ctors[typ]
+ if ctor == nil {
+ return nil, fmt.Errorf("unknown instance type '%v'", typ)
+ }
+ return ctor(env)
+}
+
+// Register registers a new VM type within the package.
+func Register(typ string, ctor ctorFunc) {
+ ctors[typ] = ctor
+}
+
+var (
+ // Close to interrupt all pending operations in all VMs.
+ Shutdown = make(chan struct{})
+ TimeoutErr = errors.New("timeout")
+
+ ctors = make(map[string]ctorFunc)
+)
+
+type ctorFunc func(env *Env) (Pool, error)