1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.compatibility.common.util;
18 
19 import com.android.tradefed.device.DeviceNotAvailableException;
20 import com.android.tradefed.device.ITestDevice;
21 import com.android.tradefed.log.LogUtil.CLog;
22 
23 import java.util.HashSet;
24 import java.util.InputMismatchException;
25 import java.util.Scanner;
26 import java.util.Set;
27 import java.util.regex.Pattern;
28 
29 /** Crawls /proc to find processes that are running as root. */
30 public class RootProcessScanner {
31 
32     private Set<String> mPidDirs;
33     private ITestDevice mDevice;
34 
RootProcessScanner(ITestDevice device)35     public RootProcessScanner(ITestDevice device) throws DeviceNotAvailableException {
36         mDevice = device;
37         mPidDirs = new HashSet<>();
38         String lsOutput = device.executeShellCommand("ls -F /proc | grep /$"); // directories only
39         String[] lines = lsOutput.split("/?\r?\n"); // split by line, shave "/" suffix if present
40         for (String line : lines) {
41             if (Pattern.matches("\\d+", line)) {
42                 mPidDirs.add(String.format("/proc/%s", line));
43             }
44         }
45     }
46 
47     /** Processes that are allowed to run as root. */
48     private static final Pattern ROOT_PROCESS_WHITELIST_PATTERN = getRootProcessWhitelistPattern(
49             "debuggerd",
50             "debuggerd64",
51             "healthd",
52             "init",
53             "installd",
54             "lmkd",
55             "netd",
56             "servicemanager",
57             "ueventd",
58             "vold",
59             "watchdogd",
60             "zygote"
61     );
62 
63     /** Combine the individual patterns into one super pattern. */
getRootProcessWhitelistPattern(String... patterns)64     private static Pattern getRootProcessWhitelistPattern(String... patterns) {
65         StringBuilder rootProcessPattern = new StringBuilder();
66         for (int i = 0; i < patterns.length; i++) {
67             rootProcessPattern.append(patterns[i]);
68             if (i + 1 < patterns.length) {
69                 rootProcessPattern.append('|');
70             }
71         }
72         return Pattern.compile(rootProcessPattern.toString());
73     }
74 
75     /**
76      * Get the names of approved or unapproved root processes running on the system.
77      * @param approved whether to retrieve approved (true) or unapproved (false) processes
78      * @return names of approved or unapproved root processes running on the system
79      */
getRootProcesses(boolean approved)80     public Set<String> getRootProcesses(boolean approved)
81             throws DeviceNotAvailableException, MalformedStatMException {
82         Set<String> rootProcessDirs = getRootProcessDirs(approved);
83         Set<String> rootProcessNames = new HashSet<>();
84         for (String dir : rootProcessDirs) {
85             rootProcessNames.add(getProcessName(dir));
86         }
87         return rootProcessNames;
88     }
89 
getRootProcessDirs(boolean approved)90     private Set<String> getRootProcessDirs(boolean approved)
91             throws DeviceNotAvailableException, MalformedStatMException {
92         Set<String> rootProcesses = new HashSet<>();
93         if (mPidDirs != null && mPidDirs.size() > 0) {
94             for (String processDir : mPidDirs) {
95                 if (isRootProcessDir(processDir, approved)) {
96                     rootProcesses.add(processDir);
97                 }
98             }
99         } else {
100             CLog.e("RootProcessScanner Failed to collect PID directories.");
101         }
102         return rootProcesses;
103     }
104 
105     /**
106      * Returns processes in /proc that are running as root with a certain approval status.
107      * @throws FileNotFoundException
108      * @throws MalformedStatMException
109      */
isRootProcessDir(String pathname, boolean approved)110     private boolean isRootProcessDir(String pathname, boolean approved)
111             throws DeviceNotAvailableException, MalformedStatMException {
112         try {
113             return !isKernelProcess(pathname)
114                     && isRootProcess(pathname)
115                     && (isApproved(pathname) == approved);
116         } catch (InputMismatchException e) {
117             CLog.d("Path %s determined not to be a root process directory", pathname);
118             return false;
119         }
120     }
121 
isKernelProcess(String processDir)122     private boolean isKernelProcess(String processDir)
123             throws DeviceNotAvailableException, MalformedStatMException {
124         String statm = getProcessStatM(processDir);
125         try (Scanner scanner = new Scanner(statm)) {
126             boolean allZero = true;
127             for (int i = 0; i < 7; i++) {
128                 if (scanner.nextInt() != 0) {
129                     allZero = false;
130                 }
131             }
132 
133             if (scanner.hasNext()) {
134                 throw new MalformedStatMException(processDir
135                         + " statm expected to have 7 integers (man 5 proc)");
136             }
137 
138             return allZero;
139         }
140     }
141 
getProcessStatM(String processDir)142     private String getProcessStatM(String processDir) throws DeviceNotAvailableException {
143         return mDevice.executeShellCommand(String.format("cat %s/statm", processDir));
144     }
145 
146     public static class MalformedStatMException extends Exception {
MalformedStatMException(String detailMessage)147         MalformedStatMException(String detailMessage) {
148             super(detailMessage);
149         }
150     }
151 
152     /**
153      * Return whether or not this process is running as root.
154      *
155      * @param processDir with the status file
156      * @return whether or not it is a root process
157      */
isRootProcess(String processDir)158     private boolean isRootProcess(String processDir) throws DeviceNotAvailableException {
159         String status = getProcessStatus(processDir);
160         try (Scanner scanner = new Scanner(status)) {
161             findToken(scanner, "Uid:");
162             boolean rootUid = hasRootId(scanner);
163             findToken(scanner, "Gid:");
164             boolean rootGid = hasRootId(scanner);
165             return rootUid || rootGid;
166         }
167     }
168 
169     /**
170      * Return whether or not this process is approved to run as root.
171      *
172      * @param processDir with the status file
173      * @return whether or not it is a root-whitelisted process
174      * @throws FileNotFoundException
175      */
isApproved(String processDir)176     private boolean isApproved(String processDir) throws DeviceNotAvailableException {
177         String status = getProcessStatus(processDir);
178         try (Scanner scanner = new Scanner(status)) {
179             findToken(scanner, "Name:");
180             String name = scanner.next();
181             return ROOT_PROCESS_WHITELIST_PATTERN.matcher(name).matches();
182         }
183     }
184 
185     /**
186      * Get the status File path that has name:value pairs.
187      * <pre>
188      * Name:   init
189      * ...
190      * Uid:    0       0       0       0
191      * Gid:    0       0       0       0
192      * </pre>
193      */
getProcessStatus(String processDir)194     private String getProcessStatus(String processDir) throws DeviceNotAvailableException {
195         return mDevice.executeShellCommand(String.format("cat %s/status", processDir));
196     }
197 
198     /**
199      * Convenience method to move the scanner's position to the point after the given token.
200      *
201      * @param scanner to call next() until the token is found
202      * @param token to find like "Name:"
203      */
findToken(Scanner scanner, String token)204     private static void findToken(Scanner scanner, String token) {
205         while (true) {
206             String next = scanner.next();
207             if (next.equals(token)) {
208                 return;
209             }
210         }
211 
212         // Scanner will exhaust input and throw an exception before getting here.
213     }
214 
215     /**
216      * Uid and Gid lines have four values: "Uid:    0       0       0       0"
217      *
218      * @param scanner that has just processed the "Uid:" or "Gid:" token
219      * @return whether or not any of the ids are root
220      */
hasRootId(Scanner scanner)221     private static boolean hasRootId(Scanner scanner) {
222         int realUid = scanner.nextInt();
223         int effectiveUid = scanner.nextInt();
224         int savedSetUid = scanner.nextInt();
225         int fileSystemUid = scanner.nextInt();
226         return realUid == 0 || effectiveUid == 0 || savedSetUid == 0 || fileSystemUid == 0;
227     }
228 
229     /** Returns the name of the process corresponding to its process directory in /proc. */
getProcessName(String processDir)230     private String getProcessName(String processDir) throws DeviceNotAvailableException {
231         String status = getProcessStatus(processDir);
232         try (Scanner scanner = new Scanner(status)) {
233             findToken(scanner, "Name:");
234             return scanner.next();
235         }
236     }
237 }
238