| // 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. |
| |
| // +build !ppc64le |
| |
| package adb |
| |
| import ( |
| "bytes" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "regexp" |
| "sync" |
| "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" |
| ) |
| |
| func init() { |
| vmimpl.Register("adb", ctor) |
| } |
| |
| type Config struct { |
| Adb string `json:"adb"` // adb binary name ("adb" by default) |
| Devices []string `json:"devices"` // list of adb device IDs to use |
| |
| // Ensure that a device battery level is at 20+% before fuzzing. |
| // Sometimes we observe that a device can't charge during heavy fuzzing |
| // and eventually powers down (which then requires manual intervention). |
| // This option is enabled by default. Turn it off if your devices |
| // don't have battery service, or it causes problems otherwise. |
| BatteryCheck bool `json:"battery_check"` |
| } |
| |
| type Pool struct { |
| env *vmimpl.Env |
| cfg *Config |
| } |
| |
| type instance struct { |
| adbBin string |
| device string |
| console string |
| closed chan bool |
| debug bool |
| } |
| |
| func ctor(env *vmimpl.Env) (vmimpl.Pool, error) { |
| cfg := &Config{ |
| Adb: "adb", |
| BatteryCheck: true, |
| } |
| if err := config.LoadData(env.Config, cfg); err != nil { |
| return nil, fmt.Errorf("failed to parse adb vm config: %v", err) |
| } |
| if _, err := exec.LookPath(cfg.Adb); err != nil { |
| return nil, err |
| } |
| if len(cfg.Devices) == 0 { |
| return nil, fmt.Errorf("no adb devices specified") |
| } |
| devRe := regexp.MustCompile("[0-9A-F]+") |
| for _, dev := range cfg.Devices { |
| if !devRe.MatchString(dev) { |
| return nil, fmt.Errorf("invalid adb device id '%v'", dev) |
| } |
| } |
| if env.Debug { |
| cfg.Devices = cfg.Devices[:1] |
| } |
| pool := &Pool{ |
| cfg: cfg, |
| env: env, |
| } |
| return pool, nil |
| } |
| |
| func (pool *Pool) Count() int { |
| return len(pool.cfg.Devices) |
| } |
| |
| func (pool *Pool) Create(workdir string, index int) (vmimpl.Instance, error) { |
| inst := &instance{ |
| adbBin: pool.cfg.Adb, |
| device: pool.cfg.Devices[index], |
| 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 |
| } |
| inst.console = findConsole(inst.adbBin, inst.device) |
| if pool.cfg.BatteryCheck { |
| if err := inst.checkBatteryLevel(); err != nil { |
| return nil, err |
| } |
| } |
| // Remove temp files from previous runs. |
| if _, err := inst.adb("shell", "rm -Rf /data/syzkaller*"); err != nil { |
| return nil, err |
| } |
| inst.adb("shell", "echo 0 > /proc/sys/kernel/kptr_restrict") |
| closeInst = nil |
| return inst, nil |
| } |
| |
| var ( |
| consoleCacheMu sync.Mutex |
| consoleToDev = make(map[string]string) |
| devToConsole = make(map[string]string) |
| ) |
| |
| // findConsole returns console file associated with the dev device (e.g. /dev/ttyUSB0). |
| // This code was tested with Suzy-Q and Android Serial Cable (ASC). For Suzy-Q see: |
| // https://chromium.googlesource.com/chromiumos/platform/ec/+/master/docs/case_closed_debugging.md |
| // The difference between Suzy-Q and ASC is that ASC is a separate cable, |
| // so it is not possible to match USB bus/port used by adb with the serial console device; |
| // while Suzy-Q console uses the same USB calbe as adb. |
| // The overall idea is as follows. We use 'adb shell' to write a unique string onto console, |
| // then we read from all console devices and see on what console the unique string appears. |
| func findConsole(adb, dev string) string { |
| consoleCacheMu.Lock() |
| defer consoleCacheMu.Unlock() |
| if con := devToConsole[dev]; con != "" { |
| return con |
| } |
| con, err := findConsoleImpl(adb, dev) |
| if err != nil { |
| log.Logf(0, "failed to associate adb device %v with console: %v", dev, err) |
| log.Logf(0, "falling back to 'adb shell dmesg -w'") |
| log.Logf(0, "note: some bugs may be detected as 'lost connection to test machine' with no kernel output") |
| con = "adb" |
| devToConsole[dev] = con |
| return con |
| } |
| devToConsole[dev] = con |
| consoleToDev[con] = dev |
| log.Logf(0, "associating adb device %v with console %v", dev, con) |
| return con |
| } |
| |
| func findConsoleImpl(adb, dev string) (string, error) { |
| // Attempt to find an exact match, at /dev/ttyUSB.{SERIAL} |
| // This is something that can be set up on Linux via 'udev' rules |
| exactCon := "/dev/ttyUSB." + dev |
| if osutil.IsExist(exactCon) { |
| return exactCon, nil |
| } |
| |
| // Search all consoles, as described in 'findConsole' |
| consoles, err := filepath.Glob("/dev/ttyUSB*") |
| if err != nil { |
| return "", fmt.Errorf("failed to list /dev/ttyUSB devices: %v", err) |
| } |
| output := make(map[string]*[]byte) |
| errors := make(chan error, len(consoles)) |
| done := make(chan bool) |
| for _, con := range consoles { |
| if consoleToDev[con] != "" { |
| continue |
| } |
| out := new([]byte) |
| output[con] = out |
| go func(con string) { |
| tty, err := vmimpl.OpenConsole(con) |
| if err != nil { |
| errors <- err |
| return |
| } |
| defer tty.Close() |
| go func() { |
| <-done |
| tty.Close() |
| }() |
| *out, _ = ioutil.ReadAll(tty) |
| errors <- nil |
| }(con) |
| } |
| if len(output) == 0 { |
| return "", fmt.Errorf("no unassociated console devices left") |
| } |
| time.Sleep(500 * time.Millisecond) |
| unique := fmt.Sprintf(">>>%v<<<", dev) |
| cmd := osutil.Command(adb, "-s", dev, "shell", "echo", "\"<1>", unique, "\"", ">", "/dev/kmsg") |
| if out, err := cmd.CombinedOutput(); err != nil { |
| return "", fmt.Errorf("failed to run adb shell: %v\n%s", err, out) |
| } |
| time.Sleep(500 * time.Millisecond) |
| close(done) |
| |
| var anyErr error |
| for range output { |
| err := <-errors |
| if anyErr == nil && err != nil { |
| anyErr = err |
| } |
| } |
| |
| con := "" |
| for con1, out := range output { |
| if bytes.Contains(*out, []byte(unique)) { |
| if con == "" { |
| con = con1 |
| } else { |
| anyErr = fmt.Errorf("device is associated with several consoles: %v and %v", con, con1) |
| } |
| } |
| } |
| |
| if con == "" { |
| if anyErr != nil { |
| return "", anyErr |
| } |
| return "", fmt.Errorf("no console is associated with this device") |
| } |
| return con, nil |
| } |
| |
| func (inst *instance) Forward(port int) (string, error) { |
| var err error |
| for i := 0; i < 1000; i++ { |
| devicePort := vmimpl.RandomPort() |
| _, err = inst.adb("reverse", fmt.Sprintf("tcp:%v", devicePort), fmt.Sprintf("tcp:%v", port)) |
| if err == nil { |
| return fmt.Sprintf("127.0.0.1:%v", devicePort), nil |
| } |
| } |
| return "", err |
| } |
| |
| func (inst *instance) adb(args ...string) ([]byte, error) { |
| if inst.debug { |
| log.Logf(0, "executing adb %+v", args) |
| } |
| args = append([]string{"-s", inst.device}, args...) |
| out, err := osutil.RunCmd(time.Minute, "", inst.adbBin, args...) |
| if inst.debug { |
| log.Logf(0, "adb returned") |
| } |
| return out, err |
| } |
| |
| func (inst *instance) repair() error { |
| // Assume that the device is in a bad state initially and reboot it. |
| // Ignore errors, maybe we will manage to reboot it anyway. |
| inst.waitForSSH() |
| // History: adb reboot episodically hangs, so we used a more reliable way: |
| // using syz-executor to issue reboot syscall. However, this has stopped |
| // working, probably due to the introduction of seccomp. Therefore, |
| // we revert this to `adb shell reboot` in the meantime, until a more |
| // reliable solution can be sought out. |
| if _, err := inst.adb("shell", "reboot"); err != nil { |
| return err |
| } |
| // Now give it another 5 minutes to boot. |
| if !vmimpl.SleepInterruptible(10 * time.Second) { |
| return fmt.Errorf("shutdown in progress") |
| } |
| if err := inst.waitForSSH(); err != nil { |
| return err |
| } |
| // Switch to root for userdebug builds. |
| inst.adb("root") |
| return inst.waitForSSH() |
| } |
| |
| func (inst *instance) waitForSSH() error { |
| var err error |
| for i := 0; i < 300; i++ { |
| if !vmimpl.SleepInterruptible(time.Second) { |
| return fmt.Errorf("shutdown in progress") |
| } |
| if _, err = inst.adb("shell", "pwd"); err == nil { |
| return nil |
| } |
| } |
| return fmt.Errorf("instance is dead and unrepairable: %v", err) |
| } |
| |
| func (inst *instance) checkBatteryLevel() error { |
| const ( |
| minLevel = 20 |
| requiredLevel = 30 |
| ) |
| val, err := inst.getBatteryLevel(30) |
| if err != nil { |
| return err |
| } |
| if val >= minLevel { |
| log.Logf(0, "device %v: battery level %v%%, OK", inst.device, val) |
| return nil |
| } |
| for { |
| log.Logf(0, "device %v: battery level %v%%, waiting for %v%%", inst.device, val, requiredLevel) |
| if !vmimpl.SleepInterruptible(time.Minute) { |
| return nil |
| } |
| val, err = inst.getBatteryLevel(0) |
| if err != nil { |
| return err |
| } |
| if val >= requiredLevel { |
| break |
| } |
| } |
| return nil |
| } |
| |
| func (inst *instance) getBatteryLevel(numRetry int) (int, error) { |
| out, err := inst.adb("shell", "dumpsys battery | grep level:") |
| |
| // allow for retrying for devices that does not boot up so fast |
| for ; numRetry >= 0 && err != nil; numRetry-- { |
| if numRetry > 0 { |
| // sleep for 5 seconds before retrying |
| time.Sleep(5 * time.Second) |
| out, err = inst.adb("shell", "dumpsys battery | grep level:") |
| } else { |
| if err != nil { |
| return 0, err |
| } |
| } |
| } |
| val := 0 |
| for _, c := range out { |
| if c >= '0' && c <= '9' { |
| val = val*10 + int(c) - '0' |
| continue |
| } |
| if val != 0 { |
| break |
| } |
| } |
| if val == 0 { |
| return 0, fmt.Errorf("failed to parse 'dumpsys battery' output: %s", out) |
| } |
| return val, nil |
| } |
| |
| func (inst *instance) Close() { |
| close(inst.closed) |
| } |
| |
| func (inst *instance) Copy(hostSrc string) (string, error) { |
| vmDst := filepath.Join("/data", filepath.Base(hostSrc)) |
| if _, err := inst.adb("push", hostSrc, vmDst); err != nil { |
| return "", err |
| } |
| return vmDst, nil |
| } |
| |
| func (inst *instance) Run(timeout time.Duration, stop <-chan bool, command string) ( |
| <-chan []byte, <-chan error, error) { |
| var tty io.ReadCloser |
| var err error |
| if inst.console == "adb" { |
| tty, err = vmimpl.OpenAdbConsole(inst.adbBin, inst.device) |
| } else { |
| tty, err = vmimpl.OpenConsole(inst.console) |
| } |
| if err != nil { |
| return nil, nil, err |
| } |
| |
| adbRpipe, adbWpipe, err := osutil.LongPipe() |
| if err != nil { |
| tty.Close() |
| return nil, nil, err |
| } |
| if inst.debug { |
| log.Logf(0, "starting: adb shell %v", command) |
| } |
| adb := osutil.Command(inst.adbBin, "-s", inst.device, "shell", "cd /data; "+command) |
| adb.Stdout = adbWpipe |
| adb.Stderr = adbWpipe |
| if err := adb.Start(); err != nil { |
| tty.Close() |
| adbRpipe.Close() |
| adbWpipe.Close() |
| return nil, nil, fmt.Errorf("failed to start adb: %v", err) |
| } |
| adbWpipe.Close() |
| |
| var tee io.Writer |
| if inst.debug { |
| tee = os.Stdout |
| } |
| merger := vmimpl.NewOutputMerger(tee) |
| merger.Add("console", tty) |
| merger.Add("adb", adbRpipe) |
| |
| return vmimpl.Multiplex(adb, merger, tty, timeout, stop, inst.closed, inst.debug) |
| } |
| |
| func (inst *instance) Diagnose() bool { |
| return false |
| } |