Report Unwhitelisted Root Processes
Bug 2738144
Scan for unwhitelisted root processes and report them in the CTS
results device summary. This initially was designed to be part
of a unit test, but the processes vary too much across devices.
Change-Id: I28ee6cf563c9e7073836e3534703c33c8925458f
diff --git a/tools/device-setup/TestDeviceSetup/src/android/tests/getinfo/DeviceInfoInstrument.java b/tools/device-setup/TestDeviceSetup/src/android/tests/getinfo/DeviceInfoInstrument.java
index 283605d..eaa9ca1 100644
--- a/tools/device-setup/TestDeviceSetup/src/android/tests/getinfo/DeviceInfoInstrument.java
+++ b/tools/device-setup/TestDeviceSetup/src/android/tests/getinfo/DeviceInfoInstrument.java
@@ -24,10 +24,13 @@
import android.os.Build;
import android.os.Bundle;
import android.telephony.TelephonyManager;
+import android.tests.getinfo.RootProcessScanner.MalformedStatMException;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.WindowManager;
+import java.io.FileNotFoundException;
+
public class DeviceInfoInstrument extends Instrumentation {
// Should use XML files in frameworks/base/data/etc to generate dynamically.
@@ -44,6 +47,7 @@
PackageManager.FEATURE_LIVE_WALLPAPER,
};
+ private static final String PROCESSES = "processes";
private static final String FEATURES = "features";
private static final String PHONE_NUMBER = "phoneNumber";
public static final String LOCALES = "locales";
@@ -139,6 +143,10 @@
String features = getFeatures();
addResult(FEATURES, features);
+ // processes
+ String processes = getProcesses();
+ addResult(PROCESSES, processes);
+
finish(Activity.RESULT_OK, mResults);
}
@@ -187,4 +195,25 @@
return builder.toString();
}
+
+ /**
+ * Return a semi-colon-delimited list of the root processes that were running on the phone
+ * or an error message.
+ */
+ private static String getProcesses() {
+ StringBuilder builder = new StringBuilder();
+
+ try {
+ String[] rootProcesses = RootProcessScanner.getRootProcesses();
+ for (String rootProcess : rootProcesses) {
+ builder.append(rootProcess).append(';');
+ }
+ } catch (FileNotFoundException notFound) {
+ builder.append(notFound.getMessage());
+ } catch (MalformedStatMException malformedStatM) {
+ builder.append(malformedStatM.getMessage());
+ }
+
+ return builder.toString();
+ }
}
diff --git a/tools/device-setup/TestDeviceSetup/src/android/tests/getinfo/RootProcessScanner.java b/tools/device-setup/TestDeviceSetup/src/android/tests/getinfo/RootProcessScanner.java
new file mode 100644
index 0000000..4763287
--- /dev/null
+++ b/tools/device-setup/TestDeviceSetup/src/android/tests/getinfo/RootProcessScanner.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.tests.getinfo;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Scanner;
+import java.util.regex.Pattern;
+
+/** Crawls /proc to find processes that are running as root. */
+class RootProcessScanner {
+
+ /** Processes that are allowed to run as root. */
+ private static final Pattern ROOT_PROCESS_WHITELIST_PATTERN = getRootProcessWhitelistPattern(
+ "debuggerd",
+ "init",
+ "installd",
+ "servicemanager",
+ "vold",
+ "zygote"
+ );
+
+ /** Combine the individual patterns into one super pattern. */
+ private static Pattern getRootProcessWhitelistPattern(String... patterns) {
+ StringBuilder rootProcessPattern = new StringBuilder();
+ for (int i = 0; i < patterns.length; i++) {
+ rootProcessPattern.append(patterns[i]);
+ if (i + 1 < patterns.length) {
+ rootProcessPattern.append('|');
+ }
+ }
+ return Pattern.compile(rootProcessPattern.toString());
+ }
+
+ /** Test that there are no unapproved root processes running on the system. */
+ public static String[] getRootProcesses()
+ throws FileNotFoundException, MalformedStatMException {
+ List<File> rootProcessDirs = getRootProcessDirs();
+ String[] rootProcessNames = new String[rootProcessDirs.size()];
+ for (int i = 0; i < rootProcessNames.length; i++) {
+ rootProcessNames[i] = getProcessName(rootProcessDirs.get(i));
+ }
+ return rootProcessNames;
+ }
+
+ private static List<File> getRootProcessDirs()
+ throws FileNotFoundException, MalformedStatMException {
+ File proc = new File("/proc");
+ if (!proc.exists()) {
+ throw new FileNotFoundException(proc + " is missing (man 5 proc)");
+ }
+
+ List<File> rootProcesses = new ArrayList<File>();
+ File[] processDirs = proc.listFiles();
+ if (processDirs != null && processDirs.length > 0) {
+ for (File processDir : processDirs) {
+ if (isUnapprovedRootProcess(processDir)) {
+ rootProcesses.add(processDir);
+ }
+ }
+ }
+ return rootProcesses;
+ }
+
+ /**
+ * Filters out processes in /proc that are not approved.
+ * @throws FileNotFoundException
+ * @throws MalformedStatMException
+ */
+ private static boolean isUnapprovedRootProcess(File pathname)
+ throws FileNotFoundException, MalformedStatMException {
+ return isPidDirectory(pathname)
+ && !isKernelProcess(pathname)
+ && isRootProcess(pathname);
+ }
+
+ private static boolean isPidDirectory(File pathname) {
+ return pathname.isDirectory() && Pattern.matches("\\d+", pathname.getName());
+ }
+
+ private static boolean isKernelProcess(File processDir)
+ throws FileNotFoundException, MalformedStatMException {
+ File statm = getProcessStatM(processDir);
+ Scanner scanner = null;
+ try {
+ scanner = new Scanner(statm);
+
+ boolean allZero = true;
+ for (int i = 0; i < 7; i++) {
+ if (scanner.nextInt() != 0) {
+ allZero = false;
+ }
+ }
+
+ if (scanner.hasNext()) {
+ throw new MalformedStatMException(processDir
+ + " statm expected to have 7 integers (man 5 proc)");
+ }
+
+ return allZero;
+ } finally {
+ if (scanner != null) {
+ scanner.close();
+ }
+ }
+ }
+
+ private static File getProcessStatM(File processDir) {
+ return new File(processDir, "statm");
+ }
+
+ public static class MalformedStatMException extends Exception {
+ MalformedStatMException(String detailMessage) {
+ super(detailMessage);
+ }
+ }
+
+ /**
+ * Return whether or not this process is running as root without being approved.
+ *
+ * @param processDir with the status file
+ * @return whether or not it is a unwhitelisted root process
+ * @throws FileNotFoundException
+ */
+ private static boolean isRootProcess(File processDir) throws FileNotFoundException {
+ File status = getProcessStatus(processDir);
+ Scanner scanner = null;
+ try {
+ scanner = new Scanner(status);
+
+ scanner = findToken(scanner, "Name:");
+ String name = scanner.next();
+
+ scanner = findToken(scanner, "Uid:");
+ boolean rootUid = hasRootId(scanner);
+
+ scanner = findToken(scanner, "Gid:");
+ boolean rootGid = hasRootId(scanner);
+
+ return !ROOT_PROCESS_WHITELIST_PATTERN.matcher(name).matches()
+ && (rootUid || rootGid);
+ } finally {
+ if (scanner != null) {
+ scanner.close();
+ }
+ }
+ }
+
+ /**
+ * Get the status {@link File} that has name:value pairs.
+ * <pre>
+ * Name: init
+ * ...
+ * Uid: 0 0 0 0
+ * Gid: 0 0 0 0
+ * </pre>
+ */
+ private static File getProcessStatus(File processDir) {
+ return new File(processDir, "status");
+ }
+
+ /**
+ * Convenience method to move the scanner's position to the point after the given token.
+ *
+ * @param scanner to call next() until the token is found
+ * @param token to find like "Name:"
+ * @return scanner after finding token
+ */
+ private static Scanner findToken(Scanner scanner, String token) {
+ while (true) {
+ String next = scanner.next();
+ if (next.equals(token)) {
+ return scanner;
+ }
+ }
+
+ // Scanner will exhaust input and throw an exception before getting here.
+ }
+
+ /**
+ * Uid and Gid lines have four values: "Uid: 0 0 0 0"
+ *
+ * @param scanner that has just processed the "Uid:" or "Gid:" token
+ * @return whether or not any of the ids are root
+ */
+ private static boolean hasRootId(Scanner scanner) {
+ int realUid = scanner.nextInt();
+ int effectiveUid = scanner.nextInt();
+ int savedSetUid = scanner.nextInt();
+ int fileSystemUid = scanner.nextInt();
+ return realUid == 0 || effectiveUid == 0 || savedSetUid == 0 || fileSystemUid == 0;
+ }
+
+ /** Returns the name of the process corresponding to its process directory in /proc. */
+ private static String getProcessName(File processDir) throws FileNotFoundException {
+ File status = getProcessStatus(processDir);
+ Scanner scanner = new Scanner(status);
+ try {
+ scanner = findToken(scanner, "Name:");
+ return scanner.next();
+ } finally {
+ scanner.close();
+ }
+ }
+}
diff --git a/tools/host/src/com/android/cts/TestDevice.java b/tools/host/src/com/android/cts/TestDevice.java
index a53968c..f8a0043 100644
--- a/tools/host/src/com/android/cts/TestDevice.java
+++ b/tools/host/src/com/android/cts/TestDevice.java
@@ -417,6 +417,7 @@
public static final String IMSI = "imsi";
public static final String PHONE_NUMBER = "phoneNumber";
public static final String FEATURES = "features";
+ public static final String PROCESSES = "processes";
private HashMap<String, String> mInfoMap;
@@ -757,6 +758,15 @@
public String getFeatures() {
return mInfoMap.get(FEATURES);
}
+
+ /**
+ * Get processes.
+ *
+ * @return Processes.
+ */
+ public String getProcesses() {
+ return mInfoMap.get(PROCESSES);
+ }
}
/**
diff --git a/tools/host/src/com/android/cts/TestSessionLog.java b/tools/host/src/com/android/cts/TestSessionLog.java
index f89150d..0307260 100644
--- a/tools/host/src/com/android/cts/TestSessionLog.java
+++ b/tools/host/src/com/android/cts/TestSessionLog.java
@@ -70,6 +70,7 @@
static final String ATTRIBUTE_ARCH = "arch";
static final String ATTRIBUTE_VALUE = "value";
static final String ATTRIBUTE_AVAILABLE = "available";
+ static final String ATTRIBUTE_UID = "uid";
static final String ATTRIBUTE_PASS = "pass";
static final String ATTRIBUTE_FAILED = "failed";
@@ -87,6 +88,8 @@
static final String TAG_BUILD_INFO = "BuildInfo";
static final String TAG_FEATURE_INFO = "FeatureInfo";
static final String TAG_FEATURE = "Feature";
+ static final String TAG_PROCESS_INFO = "ProcessInfo";
+ static final String TAG_PROCESS = "Process";
static final String TAG_PHONE_SUB_INFO = "PhoneSubInfo";
static final String TAG_TEST_RESULT = "TestResult";
static final String TAG_TESTPACKAGE = "TestPackage";
@@ -328,6 +331,7 @@
deviceSettingNode.appendChild(devInfoNode);
addFeatureInfo(doc, deviceSettingNode, bldInfo);
+ addProcessInfo(doc, deviceSettingNode, bldInfo);
}
Node hostInfo = doc.createElement(TAG_HOSTINFO);
@@ -427,6 +431,46 @@
}
/**
+ * Creates a {@link #TAG_PROCESS_INFO} tag with {@link #TAG_PROCESS} elements indicating
+ * what particular processes of interest were running on the device. It parses a string from
+ * the deviceInfo argument that is in the form of "processName1;processName2;..." with a
+ * trailing semi-colon.
+ *
+ * <pre>
+ * <ProcessInfo>
+ * <Process name="long_cat_viewer" uid="0" />
+ * ...
+ * </ProcessInfo>
+ * </pre>
+ *
+ * @param document
+ * @param parentNode
+ * @param deviceInfo
+ */
+ private void addProcessInfo(Document document, Node parentNode,
+ DeviceParameterCollector deviceInfo) {
+ Node processInfo = document.createElement(TAG_PROCESS_INFO);
+ parentNode.appendChild(processInfo);
+
+ String rootProcesses = deviceInfo.getProcesses();
+ if (rootProcesses == null) {
+ rootProcesses = "";
+ }
+
+ String[] processNames = rootProcesses.split(";");
+ for (String processName : processNames) {
+ processName = processName.trim();
+ if (processName.length() > 0) {
+ Node process = document.createElement(TAG_PROCESS);
+ processInfo.appendChild(process);
+
+ setAttribute(document, process, ATTRIBUTE_NAME, processName);
+ setAttribute(document, process, ATTRIBUTE_UID, "0");
+ }
+ }
+ }
+
+ /**
* Output TestSuite and result to XML DOM Document.
*
* @param doc The document.
diff --git a/tools/host/src/res/cts_result.xsl b/tools/host/src/res/cts_result.xsl
index 0846d91..c8fe1c8 100644
--- a/tools/host/src/res/cts_result.xsl
+++ b/tools/host/src/res/cts_result.xsl
@@ -187,6 +187,16 @@
</xsl:for-each>
</TD>
</TR>
+ <TR>
+ <TD class="rowtitle">Root Processes</TD>
+ <TD>
+ <UL>
+ <xsl:for-each select="TestResult/DeviceInfo/ProcessInfo/Process[@uid='0']">
+ <LI><xsl:value-of select="@name" /></LI>
+ </xsl:for-each>
+ </UL>
+ </TD>
+ </TR>
</TABLE>
</div>
</TD>