// 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 odroid
package odroid
// #cgo pkg-config: libusb-1.0
// #include <linux/usb/ch9.h>
// #include <linux/usb/ch11.h>
// #include <libusb.h>
import "C"
import (
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"reflect"
"time"
"unsafe"
"github.com/google/syzkaller/pkg/config"
. "github.com/google/syzkaller/pkg/log"
"github.com/google/syzkaller/pkg/osutil"
"github.com/google/syzkaller/vm/vmimpl"
)
func init() {
vmimpl.Register("odroid", ctor)
}
type Config struct {
Host_Addr string // ip address of the host machine
Slave_Addr string // ip address of the Odroid board
Console string // console device name (e.g. "/dev/ttyUSB0")
Hub_Bus int // host USB bus number for the USB hub
Hub_Device int // host USB device number for the USB hub
Hub_Port int // port on the USB hub to which Odroid is connected
}
type Pool struct {
env *vmimpl.Env
cfg *Config
}
type instance struct {
cfg *Config
os string
sshkey string
closed chan bool
debug bool
}
func ctor(env *vmimpl.Env) (vmimpl.Pool, error) {
cfg := &Config{}
if err := config.LoadData(env.Config, cfg); err != nil {
return nil, fmt.Errorf("failed to parse odroid vm config: %v", err)
}
if cfg.Host_Addr == "" {
return nil, fmt.Errorf("config param host_addr is empty")
}
if cfg.Slave_Addr == "" {
return nil, fmt.Errorf("config param slave_addr is empty")
}
if cfg.Console == "" {
return nil, fmt.Errorf("config param console is empty")
}
if cfg.Hub_Bus == 0 {
return nil, fmt.Errorf("config param hub_bus is empty")
}
if cfg.Hub_Device == 0 {
return nil, fmt.Errorf("config param hub_device is empty")
}
if cfg.Hub_Port == 0 {
return nil, fmt.Errorf("config param hub_port is empty")
}
if !osutil.IxExist(cfg.Console) {
return nil, fmt.Errorf("console file '%v' does not exist", cfg.Console)
}
pool := &Pool{
cfg: cfg,
env: env,
}
return pool, nil
}
func (pool *Pool) Count() int {
return 1 // no support for multiple Odroid devices yet
}
func (pool *Pool) Create(workdir string, index int) (vmimpl.Instance, error) {
inst := &instance{
cfg: pool.cfg,
os: pool.env.OS,
sshkey: pool.env.Sshkey,
closed: make(chan bool),
debug: pool.env.Debug,
}
closeInst := inst
defer func() {
if closeInst != nil {
closeInst.Close()
}
}()
if err := inst.repair(); err != nil {
return nil, err
}
// Create working dir if doesn't exist.
inst.ssh("mkdir -p /data/")
// Remove temp files from previous runs.
inst.ssh("rm -rf /data/syzkaller-*")
closeInst = nil
return inst, nil
}
func (inst *instance) Forward(port int) (string, error) {
return fmt.Sprintf(inst.cfg.Host_Addr+":%v", port), nil
}
func (inst *instance) ssh(command string) ([]byte, error) {
if inst.debug {
Logf(0, "executing ssh %+v", command)
}
rpipe, wpipe, err := osutil.LongPipe()
if err != nil {
return nil, err
}
args := append(vmimpl.SSHArgs(inst.debug, inst.sshkey, 22), "root@"+inst.cfg.Slave_Addr, 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, err
}
wpipe.Close()
done := make(chan bool)
go func() {
select {
case <-time.After(time.Minute):
if inst.debug {
Logf(0, "ssh hanged")
}
cmd.Process.Kill()
case <-done:
}
}()
if err := cmd.Wait(); err != nil {
close(done)
out, _ := ioutil.ReadAll(rpipe)
if inst.debug {
Logf(0, "ssh failed: %v\n%s", err, out)
}
return nil, fmt.Errorf("ssh %+v failed: %v\n%s", args, err, out)
}
close(done)
if inst.debug {
Logf(0, "ssh returned")
}
out, _ := ioutil.ReadAll(rpipe)
return out, nil
}
func switchPortPower(busNum, deviceNum, portNum int, power bool) error {
var context *C.libusb_context
if err := C.libusb_init(&context); err != 0 {
return fmt.Errorf("failed to init libusb: %v\n", err)
}
defer C.libusb_exit(context)
var rawList **C.libusb_device
numDevices := int(C.libusb_get_device_list(context, &rawList))
if numDevices < 0 {
return fmt.Errorf("failed to init libusb: %v", numDevices)
}
defer C.libusb_free_device_list(rawList, 1)
var deviceList []*C.libusb_device
*(*reflect.SliceHeader)(unsafe.Pointer(&deviceList)) = reflect.SliceHeader{
Data: uintptr(unsafe.Pointer(rawList)),
Len: numDevices,
Cap: numDevices,
}
var hub *C.libusb_device
for i := 0; i < numDevices; i++ {
var desc C.struct_libusb_device_descriptor
if err := C.libusb_get_device_descriptor(deviceList[i], &desc); err != 0 {
return fmt.Errorf("failed to get device descriptor: %v", err)
}
if desc.bDeviceClass != C.USB_CLASS_HUB {
continue
}
if C.libusb_get_bus_number(deviceList[i]) != C.uint8_t(busNum) {
continue
}
if C.libusb_get_device_address(deviceList[i]) != C.uint8_t(deviceNum) {
continue
}
hub = deviceList[i]
break
}
if hub == nil {
return fmt.Errorf("hub not found: bus: %v, device: %v", busNum, deviceNum)
}
var handle *C.libusb_device_handle
if err := C.libusb_open(hub, &handle); err != 0 {
return fmt.Errorf("failed to open usb device: %v", err)
}
request := C.uint8_t(C.USB_REQ_CLEAR_FEATURE)
if power {
request = C.uint8_t(C.USB_REQ_SET_FEATURE)
}
port := C.uint16_t(portNum)
timeout := C.uint(1000)
if err := C.libusb_control_transfer(handle, C.USB_RT_PORT, request,
C.USB_PORT_FEAT_POWER, port, nil, 0, timeout); err < 0 {
return fmt.Errorf("failed to send control message: %v\n", err)
}
return nil
}
func (inst *instance) repair() error {
// Try to shutdown gracefully.
Logf(1, "odroid: trying to ssh")
if err := inst.waitForSSH(10 * time.Second); err == nil {
Logf(1, "odroid: ssh succeeded, shutting down now")
inst.ssh("shutdown now")
if !vmimpl.SleepInterruptible(20 * time.Second) {
return fmt.Errorf("shutdown in progress")
}
} else {
Logf(1, "odroid: ssh failed")
}
// Hard reset by turning off and back on power on a hub port.
Logf(1, "odroid: hard reset, turning off power")
if err := switchPortPower(inst.cfg.Hub_Bus, inst.cfg.Hub_Device, inst.cfg.Hub_Port, false); err != nil {
return err
}
if !vmimpl.SleepInterruptible(5 * time.Second) {
return fmt.Errorf("shutdown in progress")
}
if err := switchPortPower(inst.cfg.Hub_Bus, inst.cfg.Hub_Device, inst.cfg.Hub_Port, true); err != nil {
return err
}
// Now wait for boot.
Logf(1, "odroid: power back on, waiting for boot")
if err := inst.waitForSSH(150 * time.Second); err != nil {
return err
}
Logf(1, "odroid: boot succeeded")
return nil
}
func (inst *instance) waitForSSH(timeout time.Duration) error {
return vmimpl.WaitForSSH(inst.debug, timeout, inst.cfg.Slave_Addr, inst.sshkey, "root", inst.os, 22)
}
func (inst *instance) Close() {
close(inst.closed)
}
func (inst *instance) Copy(hostSrc string) (string, error) {
basePath := "/data/"
vmDst := filepath.Join(basePath, filepath.Base(hostSrc))
args := append(vmimpl.SCPArgs(inst.debug, inst.sshkey, 22), hostSrc, "root@"+inst.cfg.Slave_Addr+":"+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) {
tty, err := vmimpl.OpenConsole(inst.cfg.Console)
if err != nil {
return nil, nil, err
}
rpipe, wpipe, err := osutil.LongPipe()
if err != nil {
tty.Close()
return nil, nil, err
}
args := append(vmimpl.SSHArgs(inst.debug, inst.sshkey, 22),
"root@"+inst.cfg.Slave_Addr, "cd /data; "+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 {
tty.Close()
rpipe.Close()
wpipe.Close()
return nil, nil, err
}
wpipe.Close()
var tee io.Writer
if inst.debug {
tee = os.Stdout
}
merger := vmimpl.NewOutputMerger(tee)
merger.Add("console", tty)
merger.Add("ssh", rpipe)
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 <-inst.closed:
if inst.debug {
Logf(0, "instance closed")
}
signal(fmt.Errorf("instance closed"))
case err := <-merger.Err:
cmd.Process.Kill()
tty.Close()
merger.Wait()
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()
tty.Close()
merger.Wait()
cmd.Wait()
}()
return merger.Output, errc, nil
}