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
17package android.tests.getinfo;
18
19import java.io.File;
20import java.io.FileNotFoundException;
21import java.util.ArrayList;
22import java.util.List;
23import java.util.Scanner;
24import java.util.regex.Pattern;
25
26/** Crawls /proc to find processes that are running as root. */
27class RootProcessScanner {
28
29    /** Processes that are allowed to run as root. */
30    private static final Pattern ROOT_PROCESS_WHITELIST_PATTERN = getRootProcessWhitelistPattern(
31            "debuggerd",
32            "debuggerd64",
33            "healthd",
34            "init",
35            "installd",
36            "lmkd",
37            "netd",
38            "servicemanager",
39            "ueventd",
40            "vold",
41            "watchdogd",
42            "zygote"
43    );
44
45    /** Combine the individual patterns into one super pattern. */
46    private static Pattern getRootProcessWhitelistPattern(String... patterns) {
47        StringBuilder rootProcessPattern = new StringBuilder();
48        for (int i = 0; i < patterns.length; i++) {
49            rootProcessPattern.append(patterns[i]);
50            if (i + 1 < patterns.length) {
51                rootProcessPattern.append('|');
52            }
53        }
54        return Pattern.compile(rootProcessPattern.toString());
55    }
56
57    /** Test that there are no unapproved root processes running on the system. */
58    public static String[] getRootProcesses()
59            throws FileNotFoundException, MalformedStatMException {
60        List<File> rootProcessDirs = getRootProcessDirs();
61        String[] rootProcessNames = new String[rootProcessDirs.size()];
62        for (int i = 0; i < rootProcessNames.length; i++) {
63            rootProcessNames[i] = getProcessName(rootProcessDirs.get(i));
64        }
65        return rootProcessNames;
66    }
67
68    private static List<File> getRootProcessDirs()
69            throws FileNotFoundException, MalformedStatMException {
70        File proc = new File("/proc");
71        if (!proc.exists()) {
72            throw new FileNotFoundException(proc + " is missing (man 5 proc)");
73        }
74
75        List<File> rootProcesses = new ArrayList<File>();
76        File[] processDirs = proc.listFiles();
77        if (processDirs != null && processDirs.length > 0) {
78            for (File processDir : processDirs) {
79                if (isUnapprovedRootProcess(processDir)) {
80                    rootProcesses.add(processDir);
81                }
82            }
83        }
84        return rootProcesses;
85    }
86
87    /**
88     * Filters out processes in /proc that are not approved.
89     * @throws FileNotFoundException
90     * @throws MalformedStatMException
91     */
92    private static boolean isUnapprovedRootProcess(File pathname)
93            throws FileNotFoundException, MalformedStatMException {
94        return isPidDirectory(pathname)
95                && !isKernelProcess(pathname)
96                && isRootProcess(pathname);
97    }
98
99    private static boolean isPidDirectory(File pathname) {
100        return pathname.isDirectory() && Pattern.matches("\\d+", pathname.getName());
101    }
102
103    private static boolean isKernelProcess(File processDir)
104            throws FileNotFoundException, MalformedStatMException {
105        File statm = getProcessStatM(processDir);
106        Scanner scanner = null;
107        try {
108            scanner = new Scanner(statm);
109
110            boolean allZero = true;
111            for (int i = 0; i < 7; i++) {
112                if (scanner.nextInt() != 0) {
113                    allZero = false;
114                }
115            }
116
117            if (scanner.hasNext()) {
118                throw new MalformedStatMException(processDir
119                        + " statm expected to have 7 integers (man 5 proc)");
120            }
121
122            return allZero;
123        } finally {
124            if (scanner != null) {
125                scanner.close();
126            }
127        }
128    }
129
130    private static File getProcessStatM(File processDir) {
131        return new File(processDir, "statm");
132    }
133
134    public static class MalformedStatMException extends Exception {
135        MalformedStatMException(String detailMessage) {
136            super(detailMessage);
137        }
138    }
139
140    /**
141     * Return whether or not this process is running as root without being approved.
142     *
143     * @param processDir with the status file
144     * @return whether or not it is a unwhitelisted root process
145     * @throws FileNotFoundException
146     */
147    private static boolean isRootProcess(File processDir) throws FileNotFoundException {
148        File status = getProcessStatus(processDir);
149        Scanner scanner = null;
150        try {
151            scanner = new Scanner(status);
152
153            findToken(scanner, "Name:");
154            String name = scanner.next();
155
156            findToken(scanner, "Uid:");
157            boolean rootUid = hasRootId(scanner);
158
159            findToken(scanner, "Gid:");
160            boolean rootGid = hasRootId(scanner);
161
162            return !ROOT_PROCESS_WHITELIST_PATTERN.matcher(name).matches()
163                    && (rootUid || rootGid);
164        } finally {
165            if (scanner != null) {
166                scanner.close();
167            }
168        }
169    }
170
171    /**
172     * Get the status {@link File} that has name:value pairs.
173     * <pre>
174     * Name:   init
175     * ...
176     * Uid:    0       0       0       0
177     * Gid:    0       0       0       0
178     * </pre>
179     */
180    private static File getProcessStatus(File processDir) {
181        return new File(processDir, "status");
182    }
183
184    /**
185     * Convenience method to move the scanner's position to the point after the given token.
186     *
187     * @param scanner to call next() until the token is found
188     * @param token to find like "Name:"
189     */
190    private static void findToken(Scanner scanner, String token) {
191        while (true) {
192            String next = scanner.next();
193            if (next.equals(token)) {
194                return;
195            }
196        }
197
198        // Scanner will exhaust input and throw an exception before getting here.
199    }
200
201    /**
202     * Uid and Gid lines have four values: "Uid:    0       0       0       0"
203     *
204     * @param scanner that has just processed the "Uid:" or "Gid:" token
205     * @return whether or not any of the ids are root
206     */
207    private static boolean hasRootId(Scanner scanner) {
208        int realUid = scanner.nextInt();
209        int effectiveUid = scanner.nextInt();
210        int savedSetUid = scanner.nextInt();
211        int fileSystemUid = scanner.nextInt();
212        return realUid == 0 || effectiveUid == 0 || savedSetUid == 0 || fileSystemUid == 0;
213    }
214
215    /** Returns the name of the process corresponding to its process directory in /proc. */
216    private static String getProcessName(File processDir) throws FileNotFoundException {
217        File status = getProcessStatus(processDir);
218        Scanner scanner = new Scanner(status);
219        try {
220            findToken(scanner, "Name:");
221            return scanner.next();
222        } finally {
223            scanner.close();
224        }
225    }
226}
227