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>