blob: 75336664084499bf55f83f97ac1d3d8952b1ea25 [file] [log] [blame]
/*
* Copyright (C) 2014 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.security.cts;
import junit.framework.TestCase;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.FileNotFoundException;
import java.io.InputStreamReader;
import java.lang.Runtime;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.Scanner;
import java.util.Set;
/**
* Verify that the processes running within an SELinux domain are sane.
*
* TODO: Author the tests for the app contexts.
*
*/
public class SELinuxDomainTest extends TestCase {
/**
* Asserts that no processes are running in a domain.
*
* @param domain
* The domain or SELinux context to check.
*/
private void assertDomainEmpty(String domain) throws FileNotFoundException {
List<ProcessDetails> procs = ProcessDetails.getProcessMap().get(domain);
String msg = "Expected no processes in SELinux domain \"" + domain + "\""
+ " Found: \"" + procs + "\"";
assertNull(msg, procs);
}
/**
* Asserts that a domain exists and that only one, well defined, process is
* running in that domain.
*
* @param domain
* The domain or SELinux context to check.
* @param executable
* The path of the executable or application package name.
*/
private void assertDomainOne(String domain, String executable) throws FileNotFoundException {
List<ProcessDetails> procs = ProcessDetails.getProcessMap().get(domain);
String msg = "Expected 1 process in SELinux domain \"" + domain + "\""
+ " Found \"" + procs + "\"";
assertNotNull(msg, procs);
assertEquals(msg, 1, procs.size());
msg = "Expected executable \"" + executable + "\" in SELinux domain \"" + domain + "\""
+ "Found: \"" + procs + "\"";
assertEquals(msg, executable, procs.get(0).procTitle);
}
/**
* Asserts that a domain may exist. If a domain exists, the cardinality of
* the domain is verified to be 1 and that the correct process is running in
* that domain.
*
* @param domain
* The domain or SELinux context to check.
* @param executable
* The path of the executable or application package name.
*/
private void assertDomainZeroOrOne(String domain, String executable)
throws FileNotFoundException {
List<ProcessDetails> procs = ProcessDetails.getProcessMap().get(domain);
if (procs == null) {
/* not on all devices */
return;
}
String msg = "Expected 1 process in SELinux domain \"" + domain + "\""
+ " Found: \"" + procs + "\"";
assertEquals(msg, 1, procs.size());
msg = "Expected executable \"" + executable + "\" in SELinux domain \"" + domain + "\""
+ "Found: \"" + procs.get(0) + "\"";
assertEquals(msg, executable, procs.get(0).procTitle);
}
/**
* Asserts that a domain must exist, and that the cardinality is greater
* than or equal to 1.
*
* @param domain
* The domain or SELinux context to check.
* @param executables
* The path of the allowed executables or application package names.
*/
private void assertDomainN(String domain, String... executables)
throws FileNotFoundException {
List<ProcessDetails> procs = ProcessDetails.getProcessMap().get(domain);
String msg = "Expected 1 or more processes in SELinux domain \"" + domain + "\""
+ " Found \"" + procs + "\"";
assertNotNull(msg, procs);
Set<String> execList = new HashSet<String>(Arrays.asList(executables));
for (ProcessDetails p : procs) {
msg = "Expected one of \"" + execList + "\" in SELinux domain \"" + domain + "\""
+ " Found: \"" + p + "\"";
assertTrue(msg, execList.contains(p.procTitle));
}
}
/**
* Asserts that a domain, if it exists, is only running the listed executables.
*
* @param domain
* The domain or SELinux context to check.
* @param executables
* The path of the allowed executables or application package names.
*/
private void assertDomainHasExecutable(String domain, String... executables)
throws FileNotFoundException {
List<ProcessDetails> procs = ProcessDetails.getProcessMap().get(domain);
if (procs == null) {
return; // domain doesn't exist
}
Set<String> execList = new HashSet<String>(Arrays.asList(executables));
for (ProcessDetails p : procs) {
String msg = "Expected one of \"" + execList + "\" in SELinux domain \""
+ domain + "\"" + " Found: \"" + p + "\"";
assertTrue(msg, execList.contains(p.procTitle));
}
}
/* Init is always there */
public void testInitDomain() throws FileNotFoundException {
assertDomainOne("u:r:init:s0", "/init");
}
/* Ueventd is always there */
public void testUeventdDomain() throws FileNotFoundException {
assertDomainOne("u:r:ueventd:s0", "/sbin/ueventd");
}
/* Devices always have healthd */
public void testHealthdDomain() throws FileNotFoundException {
assertDomainOne("u:r:healthd:s0", "/sbin/healthd");
}
/* Servicemanager is always there */
public void testServicemanagerDomain() throws FileNotFoundException {
assertDomainOne("u:r:servicemanager:s0", "/system/bin/servicemanager");
}
/* Vold is always there */
public void testVoldDomain() throws FileNotFoundException {
assertDomainOne("u:r:vold:s0", "/system/bin/vold");
}
/* netd is always there */
public void testNetdDomain() throws FileNotFoundException {
assertDomainOne("u:r:netd:s0", "/system/bin/netd");
}
/* Debuggerd is always there */
public void testDebuggerdDomain() throws FileNotFoundException {
assertDomainN("u:r:debuggerd:s0", "/system/bin/debuggerd", "/system/bin/debuggerd64");
}
/* Surface flinger is always there */
public void testSurfaceflingerDomain() throws FileNotFoundException {
assertDomainOne("u:r:surfaceflinger:s0", "/system/bin/surfaceflinger");
}
/* Zygote is always running */
public void testZygoteDomain() throws FileNotFoundException {
assertDomainN("u:r:zygote:s0", "zygote", "zygote64");
}
/* drm server is always present */
public void testDrmServerDomain() throws FileNotFoundException {
assertDomainZeroOrOne("u:r:drmserver:s0", "/system/bin/drmserver");
}
/* Media server is always running */
public void testMediaserverDomain() throws FileNotFoundException {
assertDomainN("u:r:mediaserver:s0", "media.log", "/system/bin/mediaserver");
}
/* Installd is always running */
public void testInstalldDomain() throws FileNotFoundException {
assertDomainOne("u:r:installd:s0", "/system/bin/installd");
}
/* keystore is always running */
public void testKeystoreDomain() throws FileNotFoundException {
assertDomainOne("u:r:keystore:s0", "/system/bin/keystore");
}
/* System server better be running :-P */
public void testSystemServerDomain() throws FileNotFoundException {
assertDomainOne("u:r:system_server:s0", "system_server");
}
/*
* Some OEMs do not use sdcardd so transient. Other OEMs have multiple sdcards
* so they run the daemon multiple times.
*/
public void testSdcarddDomain() throws FileNotFoundException {
assertDomainHasExecutable("u:r:sdcardd:s0", "/system/bin/sdcard");
}
/* Watchdogd may or may not be there */
public void testWatchdogdDomain() throws FileNotFoundException {
assertDomainZeroOrOne("u:r:watchdogd:s0", "/sbin/watchdogd");
}
/* Wifi may be off so cardinality of 0 or 1 is ok */
public void testWpaDomain() throws FileNotFoundException {
assertDomainZeroOrOne("u:r:wpa:s0", "/system/bin/wpa_supplicant");
}
/*
* Nothing should be running in this domain, cardinality test is all thats
* needed
*/
public void testInitShellDomain() throws FileNotFoundException {
assertDomainEmpty("u:r:init_shell:s0");
}
/*
* Nothing should be running in this domain, cardinality test is all thats
* needed
*/
public void testRecoveryDomain() throws FileNotFoundException {
assertDomainEmpty("u:r:recovery:s0");
}
/*
* Nothing should be running in this domain, cardinality test is all thats
* needed
*/
public void testSuDomain() throws FileNotFoundException {
assertDomainEmpty("u:r:su:s0");
}
/*
* Their will at least be some kernel thread running and all kthreads should
* be in kernel context.
*/
public void testKernelDomain() throws FileNotFoundException {
String domain = "u:r:kernel:s0";
List<ProcessDetails> procs = ProcessDetails.getProcessMap().get(domain);
assertNotNull(procs);
for (ProcessDetails p : procs) {
assertTrue("Non Kernel thread \"" + p + "\" found!", p.isKernel());
}
}
private static class ProcessDetails {
public String label;
public String procTitle;
public long vSize;
public int pid;
private ProcessDetails(String procTitle, String label, long vSize, int pid) {
this.label = label;
this.procTitle = procTitle;
this.vSize = vSize;
this.pid = pid;
}
@Override
public String toString() {
return "pid: \"" + pid + "\"\tproctitle: \"" + procTitle + "\"\tlabel: \"" + label
+ "\"\tvsize: " + vSize;
}
public boolean isKernel() {
return vSize == 0;
}
private static long getVsizeFromStat(String stat) {
// Get the vSize, item #23 from the stat file
// 1 2 3 4 5 6 7 8 9 10 11
String pattern = "^\\d+ \\(\\p{Print}*\\) \\w \\d+ \\d+ \\d+ \\d+ -?\\d+ \\d+ \\d+ \\d+ "
// 12 13 14 15 16 17 18 19 20 21 22 23
+ "\\d+ \\d+ \\d+ \\d+ \\d+ \\d+ -?\\d+ -?\\d+ \\d+ \\d+ \\d+ (\\d+) .*$";
Pattern p = Pattern.compile(pattern);
Matcher m = p.matcher(stat);
assertTrue("failed match: \"" + stat + "\"", m.matches());
return Long.parseLong(m.group(1));
}
private static HashMap<String, ArrayList<ProcessDetails>> getProcessMap()
throws FileNotFoundException {
HashMap<String, ArrayList<ProcessDetails>> map = new HashMap<String, ArrayList<ProcessDetails>>();
File root = new File("/proc");
if (!root.isDirectory()) {
throw new FileNotFoundException("/proc is not a directory!");
}
for (File f : root.listFiles()) {
// We only want the pid directory entries
if (!f.isDirectory()) {
continue;
}
int pid;
try {
pid = Integer.parseInt(f.getName());
} catch (NumberFormatException e) {
continue;
}
try {
ProcessDetails p = getProcessDetails(pid, f);
ArrayList<ProcessDetails> l = map.get(p.label);
if (l == null) {
l = new ArrayList<ProcessDetails>();
map.put(p.label, l);
}
l.add(p);
} catch (FileNotFoundException e) {
// sometimes processes go away while the test is running.
// Don't freak out if this happens
}
}
return map;
}
private static ProcessDetails getProcessDetails(int pid, File f) throws FileNotFoundException {
Scanner tmp = null;
String context;
long vSize;
try {
tmp = new Scanner(new File(f, "attr/current"));
// Get the context via attr/current
context = tmp.next();
context = context.trim();
} finally {
if (tmp != null) {
tmp.close();
tmp = null;
}
}
try {
// Get the vSize, item #23 from the stat file
tmp = new Scanner(new File(f, "stat"));
String x = tmp.nextLine();
vSize = getVsizeFromStat(x);
} finally {
if (tmp != null) {
tmp.close();
tmp = null;
}
}
StringBuilder sb = new StringBuilder();
try {
tmp = new Scanner(new File(f, "cmdline"));
// Java's scanner tends to return oddly when handling
// long binary blobs. Probably some caching optimization.
while (tmp.hasNext()) {
sb.append(tmp.next().replace('\0', ' '));
}
} finally {
if (tmp != null) {
tmp.close();
}
}
// At this point we build up a valid proctitle, then split
// on whitespace to get the left portion. Which is either
// package name or process executable path. This avoids
// the comm 16 char width limitation and is limited to PAGE_SIZE
String cmdline = sb.toString().trim();
cmdline = cmdline.split("\\s+")[0];
return new ProcessDetails(cmdline, context, vSize, pid);
}
}
}