ShellUtils是Trinea为了方便开发者使用shell命令而开发的一个封装库,其本意是好的,但是如果对应要运行的脚本不够了解,就可能会引发严重的后果。
前两天在团建的时候,系统组的zuxi通信微信告诉我说在Ota14的电视上长按音量+/-键会导致crash,而且android和ios都有这个问题。并且给出log,部分log如下:
01-23 14:39:37.567 D/AndroidRuntime(24667): >>>>>> AndroidRuntime START com.android.internal.os.RuntimeInit <<<<<<
01-23 14:39:37.570 D/AndroidRuntime(24667): CheckJNI is OFF
01-23 14:39:37.581 W/WindowManager( 1698): >>> keyCode=25 down=true
01-23 14:39:37.582 I/InputDispatcher( 1698): Window 'Window{1fc069ce u0 com.helios.launcher/com.helios.launcher.LauncherActivity}' spent 17341.6ms processing the last input event: KeyEvent(deviceId=-1, source=0x00000101, action=0, flags=0x00000000, keyCode=25, scanCode=0, metaState=0x00000000, repeatCount=0), policyFlags=0x6b000000
01-23 14:39:37.583 I/Input (22179): injectKeyEvent: KeyEvent { action=ACTION_UP, keyCode=KEYCODE_VOLUME_DOWN, scanCode=0, metaState=0, flags=0x0, repeatCount=0, eventTime=9753066, downTime=9753066, deviceId=-1, source=0x101 }
01-23 14:39:37.584 W/WindowManager( 1698): >>> keyCode=24 down=true
01-23 14:39:37.584 W/TelecomManager( 1698): Telecom Service not found.
01-23 14:39:37.584 W/TelecomManager( 1698): Telecom Service not found.
01-23 14:39:37.584 W/TelecomManager( 1698): Telecom Service not found.
01-23 14:39:37.584 W/TelecomManager( 1698): Telecom Service not found.
01-23 14:39:37.598 D/AndroidRuntime(24574): Calling main entry com.android.commands.input.Input
01-23 14:39:37.598 D/HeliosServerImpl( 2309): uri : / method : GET headers : {remote-addr=192.168.1.102, accept-encoding=gzip, host=192.168.1.100:12321, http-client-ip=192.168.1.102, user-agent=okhttp/3.4.1, connection=Keep-Alive} parms : {NanoHttpd.QUERY_STRING=Action=SentKey&Event=24, Action=SentKey, Event=24} files : {}
01-23 14:39:37.599 I/HeliosServerImpl( 2309): Deal KeyEvent & key = 24
很显然这不是微鲸助手的问题,而是中间件的问题了,但是为了找到问题的根源,对中间件代码进行了一番研究,发现对应的处理逻辑如下:
new Thread(new Runnable() {
@Override
public void run() {
try {
ShellUtils.execCommand("input keyevent " + event, false);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
就不吐槽这个new Thread这种非常不节约的使用线程的方式了,进入到ShellUtils.execCommand():
public static CommandResult execCommand(String command, boolean isRoot) {
return execCommand(new String[] {command}, isRoot, true);
}
而execCommand()代码如下:
public static CommandResult execCommand(String[] commands, boolean isRoot, boolean isNeedResultMsg) {
int result = -1;
if (commands == null || commands.length == 0) {
return new CommandResult(result, null, null);
}
Process process = null;
BufferedReader successResult = null;
BufferedReader errorResult = null;
StringBuilder successMsg = null;
StringBuilder errorMsg = null;
DataOutputStream os = null;
try {
process = Runtime.getRuntime().exec("sh");//isRoot ? COMMAND_SU : COMMAND_SH
os = new DataOutputStream(process.getOutputStream());
for (String command : commands) {
if (command == null) {
continue;
}
// donnot use os.writeBytes(commmand), avoid chinese charset error
os.write(command.getBytes());
os.writeBytes(COMMAND_LINE_END);
os.flush();
}
os.writeBytes(COMMAND_EXIT);
os.flush();
result = process.waitFor();
// get command result
if (isNeedResultMsg) {
successMsg = new StringBuilder();
errorMsg = new StringBuilder();
successResult = new BufferedReader(new InputStreamReader(process.getInputStream()));
errorResult = new BufferedReader(new InputStreamReader(process.getErrorStream()));
String s;
while ((s = successResult.readLine()) != null) {
successMsg.append(s);
}
while ((s = errorResult.readLine()) != null) {
errorMsg.append(s);
}
}
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (os != null) {
os.close();
}
if (successResult != null) {
successResult.close();
}
if (errorResult != null) {
errorResult.close();
}
} catch (IOException e) {
e.printStackTrace();
}
if (process != null) {
process.destroy();
}
}
return new CommandResult(result, successMsg == null ? null : successMsg.toString(), errorMsg == null ? null
: errorMsg.toString());
}
之后跟踪到Runtime中:
public Process exec(String prog) throws java.io.IOException {
return exec(prog, null, null);
}
public Process exec(String prog, String[] envp, File directory) throws java.io.IOException {
// Sanity checks
if (prog == null) {
throw new NullPointerException("prog == null");
} else if (prog.isEmpty()) {
throw new IllegalArgumentException("prog is empty");
}
// Break down into tokens, as described in Java docs
StringTokenizer tokenizer = new StringTokenizer(prog);
int length = tokenizer.countTokens();
String[] progArray = new String[length];
for (int i = 0; i < length; i++) {
progArray[i] = tokenizer.nextToken();
}
// Delegate
return exec(progArray, envp, directory);
}
public Process exec(String[] progArray, String[] envp, File directory) throws IOException {
// ProcessManager is responsible for all argument checking.
return ProcessManager.getInstance().exec(progArray, envp, directory, false);
}
之后进入到ProcessManager中:
public Process exec(String[] taintedCommand, String[] taintedEnvironment, File workingDirectory,
boolean redirectErrorStream) throws IOException {
// Make sure we throw the same exceptions as the RI.
if (taintedCommand == null) {
throw new NullPointerException("taintedCommand == null");
}
if (taintedCommand.length == 0) {
throw new IndexOutOfBoundsException("taintedCommand.length == 0");
}
// Handle security and safety by copying mutable inputs and checking them.
String[] command = taintedCommand.clone();
String[] environment = taintedEnvironment != null ? taintedEnvironment.clone() : null;
// Check we're not passing null Strings to the native exec.
for (int i = 0; i < command.length; i++) {
if (command[i] == null) {
throw new NullPointerException("taintedCommand[" + i + "] == null");
}
}
// The environment is allowed to be null or empty, but no element may be null.
if (environment != null) {
for (int i = 0; i < environment.length; i++) {
if (environment[i] == null) {
throw new NullPointerException("taintedEnvironment[" + i + "] == null");
}
}
}
FileDescriptor in = new FileDescriptor();
FileDescriptor out = new FileDescriptor();
FileDescriptor err = new FileDescriptor();
String workingPath = (workingDirectory == null)
? null
: workingDirectory.getPath();
// Ensure onExit() doesn't access the process map before we add our
// entry.
synchronized (processReferences) {
int pid;
try {
pid = exec(command, environment, workingPath, in, out, err, redirectErrorStream);
} catch (IOException e) {
IOException wrapper = new IOException("Error running exec()."
+ " Command: " + Arrays.toString(command)
+ " Working Directory: " + workingDirectory
+ " Environment: " + Arrays.toString(environment));
wrapper.initCause(e);
throw wrapper;
}
ProcessImpl process = new ProcessImpl(pid, in, out, err);
ProcessReference processReference = new ProcessReference(process, referenceQueue);
processReferences.put(pid, processReference);
/*
* This will wake up the child monitor thread in case there
* weren't previously any children to wait on.
*/
processReferences.notifyAll();
return process;
}
}
最终是调用到了一个native方法:
private static native int exec(String[] command, String[] environment,
String workingDirectory, FileDescriptor in, FileDescriptor out,
FileDescriptor err, boolean redirectErrorStream) throws IOException;
而这个方法对应的C++方法如下:
/** Executes a command in a child process. */
static pid_t ExecuteProcess(JNIEnv* env, char** commands, char** environment,
const char* workingDirectory, jobject inDescriptor,
jobject outDescriptor, jobject errDescriptor,
jboolean redirectErrorStream) {
// Create 4 pipes: stdin, stdout, stderr, and an exec() status pipe.
int pipes[PIPE_COUNT * 2] = { -1, -1, -1, -1, -1, -1, -1, -1 };
for (int i = 0; i < PIPE_COUNT; i++) {
if (pipe(pipes + i * 2) == -1) {
jniThrowIOException(env, errno);
ClosePipes(pipes, -1);
return -1;
}
}
int stdinIn = pipes[0];
int stdinOut = pipes[1];
int stdoutIn = pipes[2];
int stdoutOut = pipes[3];
int stderrIn = pipes[4];
int stderrOut = pipes[5];
int statusIn = pipes[6];
int statusOut = pipes[7];
pid_t childPid = fork();
// If fork() failed...
if (childPid == -1) {
jniThrowIOException(env, errno);
ClosePipes(pipes, -1);
return -1;
}
// If this is the child process...
if (childPid == 0) {
// Note: We cannot malloc(3) or free(3) after this point!
// A thread in the parent that no longer exists in the child may have held the heap lock
// when we forked, so an attempt to malloc(3) or free(3) would result in deadlock.
// Replace stdin, out, and err with pipes.
dup2(stdinIn, 0);
dup2(stdoutOut, 1);
if (redirectErrorStream) {
dup2(stdoutOut, 2);
} else {
dup2(stderrOut, 2);
}
// Close all but statusOut. This saves some work in the next step.
ClosePipes(pipes, statusOut);
// Make statusOut automatically close if execvp() succeeds.
fcntl(statusOut, F_SETFD, FD_CLOEXEC);
// Close remaining unwanted open fds.
CloseNonStandardFds(statusOut);
// Switch to working directory.
if (workingDirectory != NULL) {
if (chdir(workingDirectory) == -1) {
AbortChild(statusOut);
}
}
// Set up environment.
if (environment != NULL) {
extern char** environ; // Standard, but not in any header file.
environ = environment;
}
// Execute process. By convention, the first argument in the arg array
// should be the command itself.
execvp(commands[0], commands);
AbortChild(statusOut);
}
// This is the parent process.
// Close child's pipe ends.
close(stdinIn);
close(stdoutOut);
close(stderrOut);
close(statusOut);
// Check status pipe for an error code. If execvp(2) succeeds, the other
// end of the pipe should automatically close, in which case, we'll read
// nothing.
int child_errno;
ssize_t count = TEMP_FAILURE_RETRY(read(statusIn, &child_errno, sizeof(int)));
close(statusIn);
if (count > 0) {
// chdir(2) or execvp(2) in the child failed.
// TODO: track which so we can be more specific in the detail message.
jniThrowIOException(env, child_errno);
close(stdoutIn);
close(stdinOut);
close(stderrIn);
// Reap our zombie child right away.
int status;
int rc = TEMP_FAILURE_RETRY(waitpid(childPid, &status, 0));
if (rc == -1) {
ALOGW("waitpid on failed exec failed: %s", strerror(errno));
}
return -1;
}
// Fill in file descriptor wrappers.
jniSetFileDescriptorOfFD(env, inDescriptor, stdoutIn);
jniSetFileDescriptorOfFD(env, outDescriptor, stdinOut);
jniSetFileDescriptorOfFD(env, errDescriptor, stderrIn);
return childPid;
}
/**
* Converts Java String[] to char** and delegates to ExecuteProcess().
*/
static pid_t ProcessManager_exec(JNIEnv* env, jclass, jobjectArray javaCommands,
jobjectArray javaEnvironment, jstring javaWorkingDirectory,
jobject inDescriptor, jobject outDescriptor, jobject errDescriptor,
jboolean redirectErrorStream) {
ExecStrings commands(env, javaCommands);
ExecStrings environment(env, javaEnvironment);
// Extract working directory string.
const char* workingDirectory = NULL;
if (javaWorkingDirectory != NULL) {
workingDirectory = env->GetStringUTFChars(javaWorkingDirectory, NULL);
}
pid_t result = ExecuteProcess(env, commands.get(), environment.get(), workingDirectory,
inDescriptor, outDescriptor, errDescriptor, redirectErrorStream);
// Clean up working directory string.
if (javaWorkingDirectory != NULL) {
env->ReleaseStringUTFChars(javaWorkingDirectory, workingDirectory);
}
return result;
}
显然,利用fork()创建了一个子进程,并且在父子进程中使用管道传递数据.这样就基本搞清楚了Runtime.getRuntime.exec(“sh”)的本质其实就是管道通信。
但是,即使是创建了进程,又不是Zygote创建的,为何会调用到RuntimeInit呢?
当时在这里卡了一下,后面才想到是命令input可能有问题,打开它的脚本,发现如下:
# Script to start "input" on the device, which has a very rudimentary # shell. # base=/system export CLASSPATH=$base/framework/input.jar exec app_process $base/bin com.android.commands.input.Input $*
原来是先指定input.jar这个jar包的路径,再调用app_process来执行com.android.commands.input.Input的main()方法,这样就知道原因了: app_process对应的代码在frameworkks/base/cmds/app_process/app_main.cpp这个文件中,代码如下:
int main(int argc, char* const argv[])
{
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) {
// Older kernels don't understand PR_SET_NO_NEW_PRIVS and return
// EINVAL. Don't die on such kernels.
if (errno != EINVAL) {
LOG_ALWAYS_FATAL("PR_SET_NO_NEW_PRIVS failed: %s", strerror(errno));
return 12;
}
}
AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));
// Process command line arguments
// ignore argv[0]
argc--;
argv++;
// Everything up to '--' or first non '-' arg goes to the vm.
//
// The first argument after the VM args is the "parent dir", which
// is currently unused.
//
// After the parent dir, we expect one or more the following internal
// arguments :
//
// --zygote : Start in zygote mode
// --start-system-server : Start the system server.
// --application : Start in application (stand alone, non zygote) mode.
// --nice-name : The nice name for this process.
//
// For non zygote starts, these arguments will be followed by
// the main class name. All remaining arguments are passed to
// the main method of this class.
//
// For zygote starts, all remaining arguments are passed to the zygote.
// main function.
//
// Note that we must copy argument string values since we will rewrite the
// entire argument block when we apply the nice name to argv0.
int i;
for (i = 0; i < argc; i++) {
if (argv[i][0] != '-') {
break;
}
if (argv[i][1] == '-' && argv[i][2] == 0) {
++i; // Skip --.
break;
}
runtime.addOption(strdup(argv[i]));
}
// Parse runtime arguments. Stop at first unrecognized option.
bool zygote = false;
bool startSystemServer = false;
bool application = false;
String8 niceName;
String8 className;
++i; // Skip unused "parent dir" argument.
while (i < argc) {
const char* arg = argv[i++];
if (strcmp(arg, "--zygote") == 0) {
zygote = true;
niceName = ZYGOTE_NICE_NAME;
} else if (strcmp(arg, "--start-system-server") == 0) {
startSystemServer = true;
} else if (strcmp(arg, "--application") == 0) {
application = true;
} else if (strncmp(arg, "--nice-name=", 12) == 0) {
niceName.setTo(arg + 12);
} else if (strncmp(arg, "--", 2) != 0) {
className.setTo(arg);
break;
} else {
--i;
break;
}
}
Vector<String8> args;
if (!className.isEmpty()) {
// We're not in zygote mode, the only argument we need to pass
// to RuntimeInit is the application argument.
//
// The Remainder of args get passed to startup class main(). Make
// copies of them before we overwrite them with the process name.
args.add(application ? String8("application") : String8("tool"));
runtime.setClassNameAndArgs(className, argc - i, argv + i);
} else {
// We're in zygote mode.
maybeCreateDalvikCache();
if (startSystemServer) {
args.add(String8("start-system-server"));
}
char prop[PROP_VALUE_MAX];
if (property_get(ABI_LIST_PROPERTY, prop, NULL) == 0) {
LOG_ALWAYS_FATAL("app_process: Unable to determine ABI list from property %s.",
ABI_LIST_PROPERTY);
return 11;
}
String8 abiFlag("--abi-list=");
abiFlag.append(prop);
args.add(abiFlag);
// In zygote mode, pass all remaining arguments to the zygote
// main() method.
for (; i < argc; ++i) {
args.add(String8(argv[i]));
}
}
if (!niceName.isEmpty()) {
runtime.setArgv0(niceName.string());
set_process_name(niceName.string());
}
if (zygote) {
runtime.start("com.android.internal.os.ZygoteInit", args);
} else if (className) {
runtime.start("com.android.internal.os.RuntimeInit", args);
} else {
fprintf(stderr, "Error: no class name or --zygote supplied./n");
app_usage();
LOG_ALWAYS_FATAL("app_process: no class name or --zygote supplied.");
return 10;
}
}
注意最后的判断语句,显然当传入Input类时,会调用runtim.start(“com.android.internal.os.RuntimeInit”),由于runtime是AppRuntime对象,而AppRuntime继承自AndroidRuntime,之后就会调用到AndroidRuntime::start()方法:
void AndroidRuntime::start(const char* className, const Vector<String8>& options)
{
ALOGD("/n>>>>>> AndroidRuntime START %s <<<<<</n",
className != NULL ? className : "(unknown)");
static const String8 startSystemServer("start-system-server");
/*
* 'startSystemServer == true' means runtime is obsolete and not run from
* init.rc anymore, so we print out the boot start event here.
*/
for (size_t i = 0; i < options.size(); ++i) {
if (options[i] == startSystemServer) {
/* track our progress through the boot sequence */
const int LOG_BOOT_PROGRESS_START = 3000;
LOG_EVENT_LONG(LOG_BOOT_PROGRESS_START, ns2ms(systemTime(SYSTEM_TIME_MONOTONIC)));
}
}
const char* rootDir = getenv("ANDROID_ROOT");
if (rootDir == NULL) {
rootDir = "/system";
if (!hasDir("/system")) {
LOG_FATAL("No root directory specified, and /android does not exist.");
return;
}
setenv("ANDROID_ROOT", rootDir, 1);
}
//const char* kernelHack = getenv("LD_ASSUME_KERNEL");
//ALOGD("Found LD_ASSUME_KERNEL='%s'/n", kernelHack);
/* start the virtual machine */
JniInvocation jni_invocation;
jni_invocation.Init(NULL);
JNIEnv* env;
if (startVm(&mJavaVM, &env) != 0) {
return;
}
onVmCreated(env);
/*
* Register android functions.
*/
if (startReg(env) < 0) {
ALOGE("Unable to register all android natives/n");
return;
}
/*
* We want to call main() with a String array with arguments in it.
* At present we have two arguments, the class name and an option string.
* Create an array to hold them.
*/
jclass stringClass;
jobjectArray strArray;
jstring classNameStr;
stringClass = env->FindClass("java/lang/String");
assert(stringClass != NULL);
strArray = env->NewObjectArray(options.size() + 1, stringClass, NULL);
assert(strArray != NULL);
classNameStr = env->NewStringUTF(className);
assert(classNameStr != NULL);
env->SetObjectArrayElement(strArray, 0, classNameStr);
for (size_t i = 0; i < options.size(); ++i) {
jstring optionsStr = env->NewStringUTF(options.itemAt(i).string());
assert(optionsStr != NULL);
env->SetObjectArrayElement(strArray, i + 1, optionsStr);
}
/*
* Start VM. This thread becomes the main thread of the VM, and will
* not return until the VM exits.
*/
char* slashClassName = toSlashClassName(className);
jclass startClass = env->FindClass(slashClassName);
if (startClass == NULL) {
ALOGE("JavaVM unable to locate class '%s'/n", slashClassName);
/* keep going */
} else {
jmethodID startMeth = env->GetStaticMethodID(startClass, "main",
"([Ljava/lang/String;)V");
if (startMeth == NULL) {
ALOGE("JavaVM unable to find main() in '%s'/n", className);
/* keep going */
} else {
env->CallStaticVoidMethod(startClass, startMeth, strArray);
#if 0
if (env->ExceptionCheck())
threadExitUncaughtException(env);
#endif
}
}
free(slashClassName);
ALOGD("Shutting down VM/n");
if (mJavaVM->DetachCurrentThread() != JNI_OK)
ALOGW("Warning: unable to detach main thread/n");
if (mJavaVM->DestroyJavaVM() != 0)
ALOGW("Warning: VM did not shut down cleanly/n");
}
注意在这里面打印了日志而且会创建VM,所以长按音量+/-键相当于频繁创建VM,这样会使system_server挂断,从而导致系统重启。
解决方法很简单:不要使用Runtim.exec()以及input命令的方法来实现,而是使用VolumeManager.
另外,app_process其实是一个非常重要的进程,Zygote进程其实就是由它启动的,详情可以看我的这篇博客:Zygote完全解析(1)