blob: 88330d9435d2efc330b78b951f5d1475dfc8c3eb [file] [log] [blame]
/*
* Copyright (C) 2013 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 com.android.security.tests;
import com.android.ddmlib.CollectingOutputReceiver;
import com.android.loganalysis.item.BugreportItem;
import com.android.loganalysis.item.KernelLogItem;
import com.android.loganalysis.item.SELinuxItem;
import com.android.loganalysis.parser.BugreportParser;
import com.android.loganalysis.parser.KernelLogParser;
import com.android.tradefed.config.Option;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.ByteArrayInputStreamSource;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.InputStreamSource;
import com.android.tradefed.result.LogDataType;
import com.android.tradefed.testtype.IDeviceTest;
import com.android.tradefed.testtype.IRemoteTest;
import org.junit.Assert;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* Tests that count how many SELinux Denials occurred.
*/
public class SELinuxDenialsTests implements IRemoteTest, IDeviceTest {
private ITestDevice mDevice;
private static final String ADB_SHELL_KERNEL_LOGS_CMD = "dmesg";
private static final String DMESG_OUTPUT_FILE_NAME = "dmesg_output";
@Option(name="selinux-domains-file",
description=
"Filepath to a file containing a list of names of SELinux domains to seed into the " +
"test result metrics as having 0 denials. Every line in the file that " +
"contains text and doesn't start with '#', is assumed to be an SELinux domain." +
"If a file is not specified, then the test result metrics will only " +
"contain results for domains that had 1 or more denials.",
importance=Option.Importance.ALWAYS)
private String mSELinuxDomainsFile;
@Option(name="kmsg-from-bugreport",
description="Obtain kernel logs from bugreport instead of adb shell.",
importance=Option.Importance.ALWAYS)
private boolean mKmsgFromBugreport = false;
@Override
public void setDevice(ITestDevice device) {
mDevice = device;
}
@Override
public ITestDevice getDevice() {
return mDevice;
}
/**
* Runs the SELinuxDenialsCount test.
*
* @param listener The test invocation listener.
*/
@Override
public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
Map<String, String> runMetrics = new HashMap<String, String>();
listener.testRunStarted("SELinux", 0);
/* If an SELinux domains file was specified, get all the names out of it and seed them
* into runMetrics map */
if (mSELinuxDomainsFile != null) {
CLog.d("Retrieving the list of SELinux domains from file '%s'..", mSELinuxDomainsFile);
try {
List<String> domains = getSELinuxDomainNames(mSELinuxDomainsFile);
for (String domain : domains) {
runMetrics.put(domain, "0");
}
CLog.d("%s SELinux domains retrieved from file '%s'.",
domains.size(), mSELinuxDomainsFile);
} catch (IOException e) {
listener.testRunFailed(String.format(
"IOException occurred when attempting to parse SELinux domains file " +
"'%s': %s", mSELinuxDomainsFile, e.getMessage()));
return;
}
}
// get # of denials
List<SELinuxItem> selinuxLogs = getDenialsOnStartup(listener);
// tally up how many denials occur for each selinux domain, store in HashMap
Map<String, Integer> domainTallies = new HashMap<String, Integer>();
for (SELinuxItem logItem : selinuxLogs) {
incrementMapKeyValue(domainTallies, logItem.getSContext());
}
// for each domain that had at least 1 selinux denial, put into runMetrics map
for (Map.Entry<String, Integer> domainTally : domainTallies.entrySet()) {
String key = domainTally.getKey();
String value = Integer.toString(domainTally.getValue());
runMetrics.put(key, value);
CLog.d("%s SELinux denials occurred for domain '%s'", value, key);
}
// total number of denials across all selinux domains
runMetrics.put("SELinuxDenialsOnStartup", String.format("%d", selinuxLogs.size()));
CLog.d("SELinux denials TOTAL: %d", selinuxLogs.size());
listener.testRunEnded(0, runMetrics);
}
/**
* Increments the value stored in key by 1, for the specified map.
* If the key does not exist in the map, adds the key and sets it to 1.
* If the map or the key is null, does nothing.
*
* @param map the map containing the key to be incremented
* @param key the key containing the value you wish to increment
*/
private static void incrementMapKeyValue(Map<String, Integer> map, String key) {
if (map == null || key == null) {
throw new NullPointerException();
}
Integer keyValue = map.get(key);
if (keyValue == null) {
map.put(key, 1);
} else {
map.put(key, keyValue + 1);
}
}
/**
* Reads in all of the SELinux domains from the file specified. Every line in the file that
* contains text, and doesn't start with '#', is assumed to be the name of an SELinux domain.
*
* @param filepath A (@see String} containing the path to the file to be parsed.
* @return a list containing all selinux domains
* @throws IOException
*/
private static List<String> getSELinuxDomainNames(String filepath) throws IOException {
List<String> domains = new ArrayList<String>();
BufferedReader fileReader = new BufferedReader(new FileReader(filepath));
String line = null;
try {
while ((line = fileReader.readLine()) != null) {
line = line.trim();
if (!(line.isEmpty() || line.startsWith("#"))) {
domains.add(line);
}
}
} finally {
fileReader.close();
}
return domains;
}
/**
* Retrieve the list of SELinux denials that occurred since device startup.
* This test case should be run first, directly after device flash & startup.
*
* @param listener The test invocation listener.
* @return the list of SELinux denials, or {@code null} upon failure to retrieve them
* @throws DeviceNotAvailableException
*/
private List<SELinuxItem> getDenialsOnStartup(ITestInvocationListener listener)
throws DeviceNotAvailableException {
KernelLogItem kernelLogs;
/* retrieve the kernel logs */
if (mKmsgFromBugreport) {
kernelLogs = getKernelLogsFromBugreport(captureBugreport(listener));
} else {
kernelLogs = getKernelLogsFromAdbShell(listener);
}
Assert.assertNotNull(kernelLogs);
/* retrieve the selinux logs from the kernel logs */
List<SELinuxItem> selinuxLog = kernelLogs.getSELinuxEvents();
attachLogs(listener, "SELinuxEvents", LogDataType.KERNEL_LOG,
constructSELinuxLogString(selinuxLog));
if (selinuxLog == null) {
listener.testRunFailed("failed to retrieve the selinux logs");
return null;
}
return selinuxLog;
}
/**
* Gets the Kernel logs from adb shell.
*
* @param listener The test invocation listener - for capturing dmesg output upon failure.
* @return the kernel logs, or null upon failure to retrieve the logs
* @throws DeviceNotAvailableException
*/
private KernelLogItem getKernelLogsFromAdbShell(ITestInvocationListener listener)
throws DeviceNotAvailableException {
final int MAX_TIME_MS = 2 * 60 * 1000;
final int RETRIES = 3;
CollectingOutputReceiver receiver = new CollectingOutputReceiver();
CLog.i("executing adb shell %s", ADB_SHELL_KERNEL_LOGS_CMD);
mDevice.executeShellCommand(ADB_SHELL_KERNEL_LOGS_CMD, receiver, MAX_TIME_MS,
TimeUnit.MILLISECONDS, RETRIES);
String shellOutputString = receiver.getOutput();
// split the multi-line output into List<String> so it can be passed into KernelLogParser
List<String> shellOutputStrings = new ArrayList<String>(Arrays.asList(
shellOutputString.split("\\r?\\n")));
KernelLogItem kernelLog = new KernelLogParser().parse(shellOutputStrings);
if (!hasKernelLogs(kernelLog)) {
attachLogs(listener, DMESG_OUTPUT_FILE_NAME, LogDataType.TEXT, shellOutputString);
String errorMsg = "The dmesg output doesn't contain any properly formatted log lines";
CLog.e("Error: %s. See attached log file: %s", errorMsg, DMESG_OUTPUT_FILE_NAME);
return null;
}
return kernelLog;
}
/**
* Gets a parsed BugreportItem from the given InputStreamSource.
*
* @param bugreportSource the InputStreamSource containing the bugreport
* @return the parsed BugreportItem, or null upon failure.
*/
private KernelLogItem getKernelLogsFromBugreport(InputStreamSource bugreportSource) {
KernelLogItem kernelLog = null;
try {
BugreportItem bugreport = new BugreportParser().parse(new BufferedReader(
new InputStreamReader(bugreportSource.createInputStream())));
kernelLog = bugreport.getKernelLog();
if (!hasKernelLogs(kernelLog)) {
CLog.e("The kernel log appears invalid, see the captured bugreport.");
return null;
}
} catch (IOException e) {
CLog.e(String.format("Failed to fetch and parse bugreport for device %s: %s",
mDevice.getSerialNumber(), e));
} finally {
bugreportSource.close();
}
return kernelLog;
}
/**
* Verifies whether or not the specified KernelLogItem actually contains any valid Kernel Logs.
*
* @param kernelLog the kernel log item
* @return true if the KernelLogItem contains any valid Kernel logs, false otherwise
*/
private boolean hasKernelLogs(KernelLogItem kernelLog) {
/*
* When getStartTime() returns null, this means that KernelLogParser couldn't find any
* properly formatted kernel log entries with timestamps. This should mean that the adb
* shell cmd spit out an error, so we throw an exception here.
*/
if (kernelLog == null || kernelLog.getStartTime() == null) {
return false;
}
return true;
}
/**
* Construct a list of strings out of the given list of selinux logs.
*
* @param selinuxLogs the list of kernel log items
* @return list of strings, each constructed using kernel log attributes
*/
private String constructSELinuxLogString (List<SELinuxItem> selinuxLogs) {
/* construct and attach a log showing only SELinuxDenials */
StringBuffer logStr = new StringBuffer();
for (SELinuxItem logItem : selinuxLogs) {
logStr.append((String.format("%f %s\n",
logItem.getEventTime(),
logItem.getStack())));
}
return logStr.toString();
}
/**
* Captures a bug report from the device and attaches the logs to the listener.
*
* @param listener The test invocation listener to capture the logs to.
* @return the InputStreamSource of the captured bug report
*/
private InputStreamSource captureBugreport(ITestInvocationListener listener) {
CLog.i("capturing bugreport...");
InputStreamSource bugreportSource = mDevice.getBugreport();
listener.testLog("bugreport", LogDataType.BUGREPORT, bugreportSource);
return bugreportSource;
}
/**
* Passes the specified logs string to the listener, so they can be available to result
* reporters. This is typically used to upload these logs to a test results dashboard.
*
* @param listener The test invocation listener to attach the logs to.
* @param dataName the name of the logs being attached
* @param dataType the data type of the log, ie. LogDataType.KERNEL_LOG
* @param logsStr the multi-line string containing logs
*/
private void attachLogs(ITestInvocationListener listener, String dataName,
LogDataType dataType, String logsStr) {
if (logsStr.isEmpty())
// only attaching logsStr if there's any content in it
return;
// in order to attach logs to the listener, they need to be of type InputStreamSource
try (InputStreamSource logsInputStream =
new ByteArrayInputStreamSource(logsStr.getBytes())) {
// attach logs to listener, so the logs will be available to result reporters
listener.testLog(dataName, dataType, logsInputStream);
}
}
}