| // 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. |
| |
| //go:build !ppc64le |
| // +build !ppc64le |
| |
| package adb |
| |
| import ( |
| "bytes" |
| "encoding/json" |
| "fmt" |
| "io" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "regexp" |
| "strings" |
| "sync" |
| "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/vm/vmimpl" |
| ) |
| |
| func init() { |
| vmimpl.Register("adb", ctor, false) |
| } |
| |
| type Device struct { |
| Serial string `json:"serial"` // device serial to connect |
| Console string `json:"console"` // console device name (e.g. "/dev/pts/0") |
| } |
| |
| type Config struct { |
| Adb string `json:"adb"` // adb binary name ("adb" by default) |
| Devices []json.RawMessage `json:"devices"` // list of adb devices 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"` |
| // If this option is set (default), the device is rebooted after each crash. |
| // Set it to false to disable reboots. |
| TargetReboot bool `json:"target_reboot"` |
| RepairScript string `json:"repair_script"` // script to execute before each startup |
| StartupScript string `json:"startup_script"` // script to execute after each startup |
| } |
| |
| type Pool struct { |
| env *vmimpl.Env |
| cfg *Config |
| } |
| |
| type instance struct { |
| cfg *Config |
| adbBin string |
| device string |
| console string |
| closed chan bool |
| debug bool |
| } |
| |
| var ( |
| androidSerial = "^[0-9A-Za-z]+$" |
| ipAddress = `^(?:localhost|(?:[0-9]{1,3}\.){3}[0-9]{1,3})\:(?:[0-9]{1,5})$` // cuttlefish or remote_device_proxy |
| emulatorID = `^emulator\-\d+$` |
| ) |
| |
| func loadDevice(data []byte) (*Device, error) { |
| devObj := &Device{} |
| var devStr string |
| err1 := config.LoadData(data, devObj) |
| err2 := config.LoadData(data, &devStr) |
| if err1 != nil && err2 != nil { |
| return nil, fmt.Errorf("failed to parse adb vm config: %w %w", err1, err2) |
| } |
| if err2 == nil { |
| devObj.Serial = devStr |
| } |
| return devObj, nil |
| } |
| |
| func ctor(env *vmimpl.Env) (vmimpl.Pool, error) { |
| cfg := &Config{ |
| Adb: "adb", |
| BatteryCheck: true, |
| TargetReboot: true, |
| } |
| if err := config.LoadData(env.Config, cfg); err != nil { |
| return nil, fmt.Errorf("failed to parse adb vm config: %w", 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") |
| } |
| // Device should be either regular serial number, a valid Cuttlefish ID, or an Android Emulator ID. |
| devRe := regexp.MustCompile(fmt.Sprintf("%s|%s|%s", androidSerial, ipAddress, emulatorID)) |
| for _, dev := range cfg.Devices { |
| device, err := loadDevice(dev) |
| if err != nil { |
| return nil, err |
| } |
| if !devRe.MatchString(device.Serial) { |
| return nil, fmt.Errorf("invalid adb device id '%v'", device.Serial) |
| } |
| } |
| 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) { |
| device, err := loadDevice(pool.cfg.Devices[index]) |
| if err != nil { |
| return nil, err |
| } |
| inst := &instance{ |
| cfg: pool.cfg, |
| adbBin: pool.cfg.Adb, |
| device: device.Serial, |
| console: device.Console, |
| 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 |
| } |
| if inst.console == "" { |
| inst.console = findConsole(inst.adbBin, inst.device) |
| } |
| log.Logf(0, "associating adb device %v with console %v", inst.device, inst.console) |
| if pool.cfg.BatteryCheck { |
| if err := inst.checkBatteryLevel(); err != nil { |
| return nil, err |
| } |
| } |
| // Remove temp files from previous runs. |
| // rm chokes on bad symlinks so we must remove them first |
| if _, err := inst.adb("shell", "ls /data/syzkaller*"); err == nil { |
| if _, err := inst.adb("shell", "find /data/syzkaller* 2>&1 | grep 'No such file' "+ |
| "| sed 's/.*\\/data/\\/data/;s/:.*//' | xargs -r unlink"); err != nil { |
| return nil, err |
| } |
| 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) |
| ) |
| |
| func parseAdbOutToInt(out []byte) int { |
| val := 0 |
| for _, c := range out { |
| if c >= '0' && c <= '9' { |
| val = val*10 + int(c) - '0' |
| continue |
| } |
| if val != 0 { |
| break |
| } |
| } |
| return val |
| } |
| |
| // 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 |
| 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: %w", 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, _ = io.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: %w\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) { |
| return inst.adbWithTimeout(time.Minute, args...) |
| } |
| |
| func (inst *instance) adbWithTimeout(timeout time.Duration, 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(timeout, "", inst.adbBin, args...) |
| if inst.debug { |
| log.Logf(0, "adb returned") |
| } |
| return out, err |
| } |
| |
| func (inst *instance) waitForBootCompletion() { |
| // ADB connects to a phone and starts syz-fuzzer while the phone is still booting. |
| // This enables syzkaller to create a race condition which in certain cases doesn't |
| // allow the phone to finalize initialization. |
| // To determine whether a system has booted and started all system processes and |
| // services we wait for a process named 'com.android.systemui' to start. It's possible |
| // that in the future a new devices which doesn't have 'systemui' process will be fuzzed |
| // with adb, in this case this code should be modified with a new process name to search for. |
| log.Logf(2, "waiting for boot completion") |
| |
| sleepTime := 5 |
| sleepDuration := time.Duration(sleepTime) * time.Second |
| maxWaitTime := 60 * 3 // 3 minutes to wait until boot completion |
| maxRetries := maxWaitTime / sleepTime |
| i := 0 |
| for ; i < maxRetries; i++ { |
| time.Sleep(sleepDuration) |
| |
| if out, err := inst.adb("shell", "pgrep systemui | wc -l"); err == nil { |
| count := parseAdbOutToInt(out) |
| if count != 0 { |
| log.Logf(0, "boot completed") |
| break |
| } |
| } else { |
| log.Logf(0, "failed to execute command 'pgrep systemui | wc -l', %v", err) |
| break |
| } |
| } |
| if i == maxRetries { |
| log.Logf(0, "failed to determine boot completion, can't find 'com.android.systemui' process") |
| } |
| } |
| |
| 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. |
| if inst.cfg.RepairScript != "" { |
| if err := inst.runScript(inst.cfg.RepairScript); err != nil { |
| return err |
| } |
| } |
| 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 inst.cfg.TargetReboot { |
| 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") |
| inst.waitForSSH() |
| inst.waitForBootCompletion() |
| |
| // Mount debugfs. |
| if _, err := inst.adb("shell", "ls /sys/kernel/debug/kcov"); err != nil { |
| log.Logf(2, "debugfs was unmounted mounting") |
| // This prop only exist on Android 12+ |
| inst.adb("shell", "setprop persist.dbg.keep_debugfs_mounted 1") |
| if _, err := inst.adb("shell", "mount -t debugfs debugfs /sys/kernel/debug "+ |
| "&& chmod 0755 /sys/kernel/debug"); err != nil { |
| return err |
| } |
| } |
| if inst.cfg.StartupScript != "" { |
| if err := inst.runScript(inst.cfg.StartupScript); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| func (inst *instance) runScript(script string) error { |
| log.Logf(2, "adb: executing %s", script) |
| // Execute the contents of the script. |
| contents, err := os.ReadFile(script) |
| if err != nil { |
| return fmt.Errorf("unable to read %s: %w", script, err) |
| } |
| c := string(contents) |
| output, err := osutil.RunCmd(5*time.Minute, "", "sh", "-c", c) |
| if err != nil { |
| return fmt.Errorf("failed to execute %s: %w", script, err) |
| } |
| log.Logf(2, "adb: execute %s output\n%s", script, output) |
| log.Logf(2, "adb: done executing %s", script) |
| return nil |
| } |
| |
| func (inst *instance) waitForSSH() error { |
| if !vmimpl.SleepInterruptible(time.Second) { |
| return fmt.Errorf("shutdown in progress") |
| } |
| |
| if _, err := inst.adbWithTimeout(10*time.Minute, "wait-for-device"); err != nil { |
| return fmt.Errorf("instance is dead and unrepairable: %w", err) |
| } |
| |
| return nil |
| } |
| |
| 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:") |
| } |
| } |
| if err != nil { |
| return 0, err |
| } |
| val := parseAdbOutToInt(out) |
| 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 |
| } |
| |
| // Check if the device is cuttlefish on remote vm. |
| func isRemoteCuttlefish(dev string) (bool, string) { |
| if !strings.Contains(dev, ":") { |
| return false, "" |
| } |
| ip := strings.Split(dev, ":")[0] |
| if ip == "localhost" || ip == "0.0.0.0" || ip == "127.0.0.1" { |
| return false, ip |
| } |
| return true, ip |
| } |
| |
| 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 ok, ip := isRemoteCuttlefish(inst.device); ok { |
| tty, err = vmimpl.OpenRemoteKernelLog(ip, inst.console) |
| } else 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: %w", 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(rep *report.Report) ([]byte, bool) { |
| return nil, false |
| } |