转载

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

引言

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

1.起因

前两天在团建的时候,系统组的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)

原文  http://blog.imallen.wang/blog/2017/01/26/shen-yong-shellutils-cong-[?]-ge-zhong-jian-jian-dao-zhi-de-crashshuo-qi/
正文到此结束
Loading...