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