慎用ShellUtils:从一个中间件导致的crash说起

引言

ShellUtils是Trinea为了方便开发者使用shell命令而开发的一个封装库,其本意是好的,但是如果对应要运行的脚本不够了解,就可能会引发严重的后果

1.起因

前两天在团建的时候,系统组的zuxi通信微信告诉我说在Ota14的电视上长按音量+/-键会导致crash,而且android和ios都有这个问题。并且给出log,部分log如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
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

很显然这不是微鲸助手的问题,而是中间件的问题了,但是为了找到问题的根源,对中间件代码进行了一番研究,发现对应的处理逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
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():

1
2
3
4
public static CommandResult execCommand(String command, boolean isRoot) {
return execCommand(new String[] {command}, isRoot, true);
}

而execCommand()代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
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中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
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方法:

1
2
3
private static native int exec(String[] command, String[] environment,
String workingDirectory, FileDescriptor in, FileDescriptor out,
FileDescriptor err, boolean redirectErrorStream) throws IOException;

而这个方法对应的C++方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
/** 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可能有问题,打开它的脚本,发现如下:

1
2
3
4
5
6
# 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这个文件中,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
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()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
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)