blob: ff6a66fdce1792fd8d882bb749ecdc1e70e5ff5b [file] [log] [blame]
// 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
}