1 package android.cts.security; 2 3 import static android.security.cts.SELinuxHostTest.copyResourceToTempFile; 4 import static android.security.cts.SELinuxHostTest.getDevicePolicyFile; 5 import static android.security.cts.SELinuxHostTest.isMac; 6 7 import com.android.tradefed.device.ITestDevice; 8 import com.android.tradefed.device.DeviceNotAvailableException; 9 import com.android.tradefed.testtype.DeviceTestCase; 10 11 import java.io.ByteArrayOutputStream; 12 import java.io.File; 13 import java.io.InputStream; 14 import java.io.IOException; 15 import java.nio.file.Files; 16 import java.util.ArrayList; 17 import java.util.Arrays; 18 import java.util.HashSet; 19 import java.util.List; 20 import java.util.Set; 21 import java.util.concurrent.Callable; 22 import java.util.concurrent.ExecutionException; 23 import java.util.concurrent.Executors; 24 import java.util.concurrent.ExecutorService; 25 import java.util.concurrent.Future; 26 27 public class FileSystemPermissionTest extends DeviceTestCase { 28 29 /** 30 * A reference to the device under test. 31 */ 32 private ITestDevice mDevice; 33 34 /** 35 * Used to build the find command for finding insecure file system components 36 */ 37 private static final String INSECURE_DEVICE_ADB_COMMAND = "find %s -type %s -perm /o=rwx 2>/dev/null"; 38 39 @Override setUp()40 protected void setUp() throws Exception { 41 super.setUp(); 42 mDevice = getDevice(); 43 } 44 testAllBlockDevicesAreSecure()45 public void testAllBlockDevicesAreSecure() throws Exception { 46 Set<String> insecure = getAllInsecureDevicesInDirAndSubdir("/dev", "b"); 47 assertTrue("Found insecure block devices: " + insecure.toString(), 48 insecure.isEmpty()); 49 } 50 51 /** 52 * Searches for all world accessable files, note this may need sepolicy to search the desired 53 * location and stat files. 54 * @path The path to search, must be a directory. 55 * @type The type of file to search for, must be a valid find command argument to the type 56 * option. 57 * @returns The set of insecure fs objects found. 58 */ getAllInsecureDevicesInDirAndSubdir(String path, String type)59 private Set<String> getAllInsecureDevicesInDirAndSubdir(String path, String type) throws DeviceNotAvailableException { 60 61 String cmd = getInsecureDeviceAdbCommand(path, type); 62 String output = mDevice.executeShellCommand(cmd); 63 // Splitting an empty string results in an array of an empty string. 64 String [] found = output.length() > 0 ? output.split("\\s") : new String[0]; 65 return new HashSet<String>(Arrays.asList(found)); 66 } 67 getInsecureDeviceAdbCommand(String path, String type)68 private static String getInsecureDeviceAdbCommand(String path, String type) { 69 return String.format(INSECURE_DEVICE_ADB_COMMAND, path, type); 70 } 71 72 private static String HW_RNG_DEVICE = "/dev/hw_random"; 73 testDevHwRandomPermissions()74 public void testDevHwRandomPermissions() throws Exception { 75 // This test asserts that, if present, /dev/hw_random must: 76 // 1. Be owned by UID root 77 // 2. Not allow any world read, write, or execute permissions. The reason 78 // for being not readable by all/other is to avoid apps reading from this device. 79 // Firstly, /dev/hw_random is not public API for apps. Secondly, apps might erroneously 80 // use the output of Hardware RNG as trusted random output. Android does not trust output 81 // of /dev/hw_random. HW RNG output is only used for mixing into Linux RNG as untrusted 82 // input. 83 // 3. Be a character device with major:minor 10:183 -- hwrng kernel driver is using MAJOR 10 84 // and MINOR 183 85 // 4. Be openable and readable by system_server according to SELinux policy 86 87 if (!mDevice.doesFileExist(HW_RNG_DEVICE)) { 88 // Hardware RNG device is missing. This is OK because it is not required to be exposed 89 // on all devices. 90 return; 91 } 92 93 String command = "ls -l " + HW_RNG_DEVICE; 94 String output = mDevice.executeShellCommand(command).trim(); 95 if (!output.endsWith(" " + HW_RNG_DEVICE)) { 96 fail("Unexpected output from " + command + ": \"" + output + "\""); 97 } 98 String[] outputWords = output.split("\\s"); 99 assertEquals("Wrong device type on " + HW_RNG_DEVICE, "c", outputWords[0].substring(0, 1)); 100 assertEquals("Wrong world file mode on " + HW_RNG_DEVICE, "---", outputWords[0].substring(7)); 101 assertEquals("Wrong owner of " + HW_RNG_DEVICE, "root", outputWords[2]); 102 assertEquals("Wrong device major on " + HW_RNG_DEVICE, "10,", outputWords[4]); 103 assertEquals("Wrong device minor on " + HW_RNG_DEVICE, "183", outputWords[5]); 104 105 command = "ls -Z " + HW_RNG_DEVICE; 106 output = mDevice.executeShellCommand(command).trim(); 107 assertEquals( 108 "Wrong SELinux label on " + HW_RNG_DEVICE, 109 "u:object_r:hw_random_device:s0 " + HW_RNG_DEVICE, 110 output); 111 112 File sepolicy = getDevicePolicyFile(mDevice); 113 output = 114 new String( 115 execSearchPolicy( 116 "--allow", 117 "-s", "system_server", 118 "-t", "hw_random_device", 119 "-c", "chr_file", 120 "-p", "open", 121 sepolicy.getPath())); 122 if (output.trim().isEmpty()) { 123 fail("SELinux policy does not permit system_server to open " + HW_RNG_DEVICE); 124 } 125 output = 126 new String( 127 execSearchPolicy( 128 "--allow", 129 "-s", "system_server", 130 "-t", "hw_random_device", 131 "-c", "chr_file", 132 "-p", "read", 133 sepolicy.getPath())); 134 if (output.trim().isEmpty()) { 135 fail("SELinux policy does not permit system_server to read " + HW_RNG_DEVICE); 136 } 137 } 138 139 /** 140 * Executes {@code searchpolicy} executable with the provided parameters and returns the 141 * contents of standard output. 142 * 143 * @throws IOException if execution of searchpolicy fails, returns non-zero error code, or 144 * non-empty stderr 145 */ execSearchPolicy(String... args)146 private static byte[] execSearchPolicy(String... args) 147 throws InterruptedException, IOException { 148 File tmpDir = Files.createTempDirectory("searchpolicy").toFile(); 149 try { 150 String[] envp; 151 File libsepolwrap; 152 if (isMac()) { 153 libsepolwrap = copyResourceToTempFile("/libsepolwrap.dylib"); 154 libsepolwrap = 155 Files.move( 156 libsepolwrap.toPath(), 157 new File(tmpDir, "libsepolwrap.dylib").toPath()).toFile(); 158 File libcpp = copyResourceToTempFile("/libc++.dylib"); 159 Files.move(libcpp.toPath(), new File(tmpDir, "libc++.dylib").toPath()); 160 envp = new String[] {"DYLD_LIBRARY_PATH=" + tmpDir.getAbsolutePath()}; 161 } else { 162 libsepolwrap = copyResourceToTempFile("/libsepolwrap.so"); 163 libsepolwrap = 164 Files.move( 165 libsepolwrap.toPath(), 166 new File(tmpDir, "libsepolwrap.so").toPath()).toFile(); 167 File libcpp = copyResourceToTempFile("/libc++.so"); 168 Files.move(libcpp.toPath(), new File(tmpDir, "libc++.so").toPath()); 169 envp = new String[] {"LD_LIBRARY_PATH=" + tmpDir.getAbsolutePath()}; 170 } 171 File searchpolicy = copyResourceToTempFile("/searchpolicy"); 172 searchpolicy = 173 Files.move( 174 searchpolicy.toPath(), 175 new File(tmpDir, "searchpolicy").toPath()).toFile(); 176 searchpolicy.setExecutable(true); 177 libsepolwrap.setExecutable(true); 178 List<String> cmd = new ArrayList<>(3 + args.length); 179 cmd.add(searchpolicy.getPath()); 180 cmd.add("--libpath"); 181 cmd.add(libsepolwrap.getPath()); 182 for (String arg : args) { 183 cmd.add(arg); 184 } 185 return execAndCaptureOutput(cmd.toArray(new String[0]), envp); 186 } finally { 187 // Delete tmpDir 188 File[] files = tmpDir.listFiles(); 189 if (files == null) { 190 files = new File[0]; 191 } 192 for (File f : files) { 193 f.delete(); 194 } 195 tmpDir.delete(); 196 } 197 } 198 199 /** 200 * Executes the provided command and returns the contents of standard output. 201 * 202 * @throws IOException if execution fails, returns a non-zero error code, or non-empty stderr 203 */ execAndCaptureOutput(String[] cmd, String[] envp)204 private static byte[] execAndCaptureOutput(String[] cmd, String[] envp) 205 throws InterruptedException, IOException { 206 // Start process, read its stdout and stderr in two corresponding background threads, wait 207 // for process to terminate, throw if stderr is not empty or if return code != 0. 208 final Process p = Runtime.getRuntime().exec(cmd, envp); 209 ExecutorService executorService = null; 210 try { 211 executorService = Executors.newFixedThreadPool(2); 212 Future<byte[]> stdoutContentsFuture = 213 executorService.submit(new DrainCallable(p.getInputStream())); 214 Future<byte[]> stderrContentsFuture = 215 executorService.submit(new DrainCallable(p.getErrorStream())); 216 int errorCode = p.waitFor(); 217 byte[] stderrContents = stderrContentsFuture.get(); 218 if ((errorCode != 0) || (stderrContents.length > 0)) { 219 throw new IOException( 220 cmd[0] + " failed with error code " + errorCode 221 + ": " + new String(stderrContents)); 222 } 223 return stdoutContentsFuture.get(); 224 } catch (ExecutionException e) { 225 throw new IOException("Failed to read stdout or stderr of " + cmd[0], e); 226 } finally { 227 if (executorService != null) { 228 executorService.shutdownNow(); 229 } 230 } 231 } 232 233 private static class DrainCallable implements Callable<byte[]> { 234 private final InputStream mIn; 235 DrainCallable(InputStream in)236 private DrainCallable(InputStream in) { 237 mIn = in; 238 } 239 240 @Override call()241 public byte[] call() throws IOException { 242 ByteArrayOutputStream result = new ByteArrayOutputStream(); 243 byte[] buf = new byte[16384]; 244 int chunkSize; 245 while ((chunkSize = mIn.read(buf)) != -1) { 246 result.write(buf, 0, chunkSize); 247 } 248 return result.toByteArray(); 249 } 250 } 251 } 252