blob: 2e0d0a549fc69d6a2fba34b8acb5be7ddc781b5e [file] [log] [blame]
package android.cts.security;
import static android.security.cts.SELinuxHostTest.copyResourceToTempFile;
import static android.security.cts.SELinuxHostTest.getDevicePolicyFile;
import static android.security.cts.SELinuxHostTest.isMac;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.testtype.DeviceTestCase;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
public class FileSystemPermissionTest extends DeviceTestCase {
/**
* A reference to the device under test.
*/
private ITestDevice mDevice;
/**
* Used to build the find command for finding insecure file system components
*/
private static final String INSECURE_DEVICE_ADB_COMMAND = "find %s -type %s -perm /o=rwx 2>/dev/null";
@Override
protected void setUp() throws Exception {
super.setUp();
mDevice = getDevice();
}
public void testAllBlockDevicesAreSecure() throws Exception {
Set<String> insecure = getAllInsecureDevicesInDirAndSubdir("/dev", "b");
assertTrue("Found insecure block devices: " + insecure.toString(),
insecure.isEmpty());
}
/**
* Searches for all world accessable files, note this may need sepolicy to search the desired
* location and stat files.
* @path The path to search, must be a directory.
* @type The type of file to search for, must be a valid find command argument to the type
* option.
* @returns The set of insecure fs objects found.
*/
private Set<String> getAllInsecureDevicesInDirAndSubdir(String path, String type) throws DeviceNotAvailableException {
String cmd = getInsecureDeviceAdbCommand(path, type);
String output = mDevice.executeShellCommand(cmd);
// Splitting an empty string results in an array of an empty string.
String [] found = output.length() > 0 ? output.split("\\s") : new String[0];
return new HashSet<String>(Arrays.asList(found));
}
private static String getInsecureDeviceAdbCommand(String path, String type) {
return String.format(INSECURE_DEVICE_ADB_COMMAND, path, type);
}
private static String HW_RNG_DEVICE = "/dev/hw_random";
public void testDevHwRandomPermissions() throws Exception {
// This test asserts that, if present, /dev/hw_random must:
// 1. Be owned by UID root
// 2. Not allow any world read, write, or execute permissions. The reason
// for being not readable by all/other is to avoid apps reading from this device.
// Firstly, /dev/hw_random is not public API for apps. Secondly, apps might erroneously
// use the output of Hardware RNG as trusted random output. Android does not trust output
// of /dev/hw_random. HW RNG output is only used for mixing into Linux RNG as untrusted
// input.
// 3. Be a character device with major:minor 10:183 -- hwrng kernel driver is using MAJOR 10
// and MINOR 183
// 4. Be openable and readable by system_server according to SELinux policy
if (!mDevice.doesFileExist(HW_RNG_DEVICE)) {
// Hardware RNG device is missing. This is OK because it is not required to be exposed
// on all devices.
return;
}
String command = "ls -l " + HW_RNG_DEVICE;
String output = mDevice.executeShellCommand(command).trim();
if (!output.endsWith(" " + HW_RNG_DEVICE)) {
fail("Unexpected output from " + command + ": \"" + output + "\"");
}
String[] outputWords = output.split("\\s");
assertEquals("Wrong device type on " + HW_RNG_DEVICE, "c", outputWords[0].substring(0, 1));
assertEquals("Wrong world file mode on " + HW_RNG_DEVICE, "---", outputWords[0].substring(7));
assertEquals("Wrong owner of " + HW_RNG_DEVICE, "root", outputWords[2]);
assertEquals("Wrong device major on " + HW_RNG_DEVICE, "10,", outputWords[4]);
assertEquals("Wrong device minor on " + HW_RNG_DEVICE, "183", outputWords[5]);
command = "ls -Z " + HW_RNG_DEVICE;
output = mDevice.executeShellCommand(command).trim();
assertEquals(
"Wrong SELinux label on " + HW_RNG_DEVICE,
"u:object_r:hw_random_device:s0 " + HW_RNG_DEVICE,
output);
File sepolicy = getDevicePolicyFile(mDevice);
output =
new String(
execSearchPolicy(
"--allow",
"-s", "system_server",
"-t", "hw_random_device",
"-c", "chr_file",
"-p", "open",
sepolicy.getPath()));
if (output.trim().isEmpty()) {
fail("SELinux policy does not permit system_server to open " + HW_RNG_DEVICE);
}
output =
new String(
execSearchPolicy(
"--allow",
"-s", "system_server",
"-t", "hw_random_device",
"-c", "chr_file",
"-p", "read",
sepolicy.getPath()));
if (output.trim().isEmpty()) {
fail("SELinux policy does not permit system_server to read " + HW_RNG_DEVICE);
}
}
/**
* Executes {@code searchpolicy} executable with the provided parameters and returns the
* contents of standard output.
*
* @throws IOException if execution of searchpolicy fails, returns non-zero error code, or
* non-empty stderr
*/
private static byte[] execSearchPolicy(String... args)
throws InterruptedException, IOException {
File tmpDir = Files.createTempDirectory("searchpolicy").toFile();
try {
String[] envp;
File libsepolwrap;
if (isMac()) {
libsepolwrap = copyResourceToTempFile("/libsepolwrap.dylib");
libsepolwrap =
Files.move(
libsepolwrap.toPath(),
new File(tmpDir, "libsepolwrap.dylib").toPath()).toFile();
File libcpp = copyResourceToTempFile("/libc++.dylib");
Files.move(libcpp.toPath(), new File(tmpDir, "libc++.dylib").toPath());
envp = new String[] {"DYLD_LIBRARY_PATH=" + tmpDir.getAbsolutePath()};
} else {
libsepolwrap = copyResourceToTempFile("/libsepolwrap.so");
libsepolwrap =
Files.move(
libsepolwrap.toPath(),
new File(tmpDir, "libsepolwrap.so").toPath()).toFile();
File libcpp = copyResourceToTempFile("/libc++.so");
Files.move(libcpp.toPath(), new File(tmpDir, "libc++.so").toPath());
envp = new String[] {"LD_LIBRARY_PATH=" + tmpDir.getAbsolutePath()};
}
File searchpolicy = copyResourceToTempFile("/searchpolicy");
searchpolicy =
Files.move(
searchpolicy.toPath(),
new File(tmpDir, "searchpolicy").toPath()).toFile();
searchpolicy.setExecutable(true);
libsepolwrap.setExecutable(true);
List<String> cmd = new ArrayList<>(3 + args.length);
cmd.add(searchpolicy.getPath());
cmd.add("--libpath");
cmd.add(libsepolwrap.getPath());
for (String arg : args) {
cmd.add(arg);
}
return execAndCaptureOutput(cmd.toArray(new String[0]), envp);
} finally {
// Delete tmpDir
File[] files = tmpDir.listFiles();
if (files == null) {
files = new File[0];
}
for (File f : files) {
f.delete();
}
tmpDir.delete();
}
}
/**
* Executes the provided command and returns the contents of standard output.
*
* @throws IOException if execution fails, returns a non-zero error code, or non-empty stderr
*/
private static byte[] execAndCaptureOutput(String[] cmd, String[] envp)
throws InterruptedException, IOException {
// Start process, read its stdout and stderr in two corresponding background threads, wait
// for process to terminate, throw if stderr is not empty or if return code != 0.
final Process p = Runtime.getRuntime().exec(cmd, envp);
ExecutorService executorService = null;
try {
executorService = Executors.newFixedThreadPool(2);
Future<byte[]> stdoutContentsFuture =
executorService.submit(new DrainCallable(p.getInputStream()));
Future<byte[]> stderrContentsFuture =
executorService.submit(new DrainCallable(p.getErrorStream()));
int errorCode = p.waitFor();
byte[] stderrContents = stderrContentsFuture.get();
if ((errorCode != 0) || (stderrContents.length > 0)) {
throw new IOException(
cmd[0] + " failed with error code " + errorCode
+ ": " + new String(stderrContents));
}
return stdoutContentsFuture.get();
} catch (ExecutionException e) {
throw new IOException("Failed to read stdout or stderr of " + cmd[0], e);
} finally {
if (executorService != null) {
executorService.shutdownNow();
}
}
}
private static class DrainCallable implements Callable<byte[]> {
private final InputStream mIn;
private DrainCallable(InputStream in) {
mIn = in;
}
@Override
public byte[] call() throws IOException {
ByteArrayOutputStream result = new ByteArrayOutputStream();
byte[] buf = new byte[16384];
int chunkSize;
while ((chunkSize = mIn.read(buf)) != -1) {
result.write(buf, 0, chunkSize);
}
return result.toByteArray();
}
}
}