1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.permission.cts; 18 19 import android.content.pm.ApplicationInfo; 20 import android.content.pm.PackageManager; 21 import android.os.Environment; 22 import android.system.OsConstants; 23 import android.test.AndroidTestCase; 24 import android.test.suitebuilder.annotation.MediumTest; 25 import android.test.suitebuilder.annotation.LargeTest; 26 27 import java.io.BufferedReader; 28 import java.io.File; 29 import java.io.FileFilter; 30 import java.io.FileInputStream; 31 import java.io.FileNotFoundException; 32 import java.io.FileOutputStream; 33 import java.io.FileReader; 34 import java.io.InputStream; 35 import java.io.IOException; 36 import java.io.OutputStream; 37 import java.util.concurrent.Callable; 38 import java.util.concurrent.ExecutionException; 39 import java.util.concurrent.Executors; 40 import java.util.concurrent.ExecutorService; 41 import java.util.concurrent.Future; 42 import java.util.concurrent.TimeoutException; 43 import java.util.concurrent.TimeUnit; 44 import java.util.Arrays; 45 import java.util.HashMap; 46 import java.util.HashSet; 47 import java.util.List; 48 import java.util.Set; 49 50 /** 51 * Verify certain permissions on the filesystem 52 * 53 * TODO: Combine this file with {@link android.os.cts.FileAccessPermissionTest} 54 */ 55 public class FileSystemPermissionTest extends AndroidTestCase { 56 57 @MediumTest testCreateFileHasSanePermissions()58 public void testCreateFileHasSanePermissions() throws Exception { 59 File myFile = new File(getContext().getFilesDir(), "hello"); 60 FileOutputStream stream = new FileOutputStream(myFile); 61 stream.write("hello world".getBytes()); 62 stream.close(); 63 try { 64 FileUtils.FileStatus status = new FileUtils.FileStatus(); 65 FileUtils.getFileStatus(myFile.getAbsolutePath(), status, false); 66 int expectedPerms = FileUtils.S_IFREG 67 | FileUtils.S_IWUSR 68 | FileUtils.S_IRUSR; 69 assertEquals( 70 "Newly created files should have 0600 permissions", 71 Integer.toOctalString(expectedPerms), 72 Integer.toOctalString(status.mode)); 73 } finally { 74 assertTrue(myFile.delete()); 75 } 76 } 77 78 @MediumTest testCreateDirectoryHasSanePermissions()79 public void testCreateDirectoryHasSanePermissions() throws Exception { 80 File myDir = new File(getContext().getFilesDir(), "helloDirectory"); 81 assertTrue(myDir.mkdir()); 82 try { 83 FileUtils.FileStatus status = new FileUtils.FileStatus(); 84 FileUtils.getFileStatus(myDir.getAbsolutePath(), status, false); 85 int expectedPerms = FileUtils.S_IFDIR 86 | FileUtils.S_IWUSR 87 | FileUtils.S_IRUSR 88 | FileUtils.S_IXUSR; 89 assertEquals( 90 "Newly created directories should have 0700 permissions", 91 Integer.toOctalString(expectedPerms), 92 Integer.toOctalString(status.mode)); 93 94 } finally { 95 assertTrue(myDir.delete()); 96 } 97 } 98 99 @MediumTest testOtherApplicationDirectoriesAreNotWritable()100 public void testOtherApplicationDirectoriesAreNotWritable() throws Exception { 101 Set<File> writableDirs = new HashSet<File>(); 102 List<ApplicationInfo> apps = getContext() 103 .getPackageManager() 104 .getInstalledApplications(PackageManager.GET_UNINSTALLED_PACKAGES); 105 String myAppDirectory = getContext().getApplicationInfo().dataDir; 106 for (ApplicationInfo app : apps) { 107 if (!myAppDirectory.equals(app.dataDir)) { 108 writableDirs.addAll(getWritableDirectoryiesAndSubdirectoriesOf(new File(app.dataDir))); 109 } 110 } 111 112 assertTrue("Found writable directories: " + writableDirs.toString(), 113 writableDirs.isEmpty()); 114 } 115 116 @MediumTest testApplicationParentDirectoryNotWritable()117 public void testApplicationParentDirectoryNotWritable() throws Exception { 118 String myDataDir = getContext().getApplicationInfo().dataDir; 119 File parentDir = new File(myDataDir).getParentFile(); 120 assertFalse(parentDir.toString(), isDirectoryWritable(parentDir)); 121 } 122 123 @MediumTest testDataDirectoryNotWritable()124 public void testDataDirectoryNotWritable() throws Exception { 125 assertFalse(isDirectoryWritable(Environment.getDataDirectory())); 126 } 127 128 @MediumTest testAndroidRootDirectoryNotWritable()129 public void testAndroidRootDirectoryNotWritable() throws Exception { 130 assertFalse(isDirectoryWritable(Environment.getRootDirectory())); 131 } 132 133 @MediumTest testDownloadCacheDirectoryNotWritable()134 public void testDownloadCacheDirectoryNotWritable() throws Exception { 135 assertFalse(isDirectoryWritable(Environment.getDownloadCacheDirectory())); 136 } 137 138 @MediumTest testRootDirectoryNotWritable()139 public void testRootDirectoryNotWritable() throws Exception { 140 assertFalse(isDirectoryWritable(new File("/"))); 141 } 142 143 @MediumTest testDevDirectoryNotWritable()144 public void testDevDirectoryNotWritable() throws Exception { 145 assertFalse(isDirectoryWritable(new File("/dev"))); 146 } 147 148 @MediumTest testProcDirectoryNotWritable()149 public void testProcDirectoryNotWritable() throws Exception { 150 assertFalse(isDirectoryWritable(new File("/proc"))); 151 } 152 153 @MediumTest testDevMemSane()154 public void testDevMemSane() throws Exception { 155 File f = new File("/dev/mem"); 156 assertFalse(f.canRead()); 157 assertFalse(f.canWrite()); 158 assertFalse(f.canExecute()); 159 } 160 161 @MediumTest testDevkmemSane()162 public void testDevkmemSane() throws Exception { 163 File f = new File("/dev/kmem"); 164 assertFalse(f.canRead()); 165 assertFalse(f.canWrite()); 166 assertFalse(f.canExecute()); 167 } 168 169 @MediumTest testDevPortSane()170 public void testDevPortSane() throws Exception { 171 File f = new File("/dev/port"); 172 assertFalse(f.canRead()); 173 assertFalse(f.canWrite()); 174 assertFalse(f.canExecute()); 175 } 176 177 @MediumTest testPn544Sane()178 public void testPn544Sane() throws Exception { 179 File f = new File("/dev/pn544"); 180 assertFalse(f.canRead()); 181 assertFalse(f.canWrite()); 182 assertFalse(f.canExecute()); 183 184 assertFileOwnedBy(f, "nfc"); 185 assertFileOwnedByGroup(f, "nfc"); 186 } 187 188 @MediumTest testBcm2079xSane()189 public void testBcm2079xSane() throws Exception { 190 File f = new File("/dev/bcm2079x"); 191 assertFalse(f.canRead()); 192 assertFalse(f.canWrite()); 193 assertFalse(f.canExecute()); 194 195 assertFileOwnedBy(f, "nfc"); 196 assertFileOwnedByGroup(f, "nfc"); 197 } 198 199 @MediumTest testBcm2079xi2cSane()200 public void testBcm2079xi2cSane() throws Exception { 201 File f = new File("/dev/bcm2079x-i2c"); 202 assertFalse(f.canRead()); 203 assertFalse(f.canWrite()); 204 assertFalse(f.canExecute()); 205 206 assertFileOwnedBy(f, "nfc"); 207 assertFileOwnedByGroup(f, "nfc"); 208 } 209 210 @MediumTest testDevQtaguidSane()211 public void testDevQtaguidSane() throws Exception { 212 File f = new File("/dev/xt_qtaguid"); 213 assertTrue(f.canRead()); 214 assertFalse(f.canWrite()); 215 assertFalse(f.canExecute()); 216 217 assertFileOwnedBy(f, "root"); 218 assertFileOwnedByGroup(f, "root"); 219 } 220 221 @MediumTest testProcQtaguidCtrlSane()222 public void testProcQtaguidCtrlSane() throws Exception { 223 File f = new File("/proc/net/xt_qtaguid/ctrl"); 224 assertTrue(f.canRead()); 225 assertTrue(f.canWrite()); 226 assertFalse(f.canExecute()); 227 228 assertFileOwnedBy(f, "root"); 229 assertFileOwnedByGroup(f, "net_bw_acct"); 230 } 231 232 @MediumTest testProcQtaguidStatsSane()233 public void testProcQtaguidStatsSane() throws Exception { 234 File f = new File("/proc/net/xt_qtaguid/stats"); 235 assertTrue(f.canRead()); 236 assertFalse(f.canWrite()); 237 assertFalse(f.canExecute()); 238 239 assertFileOwnedBy(f, "root"); 240 assertFileOwnedByGroup(f, "net_bw_stats"); 241 } 242 243 @MediumTest testTcpDefaultRwndSane()244 public void testTcpDefaultRwndSane() throws Exception { 245 File f = new File("/proc/sys/net/ipv4/tcp_default_init_rwnd"); 246 assertTrue(f.canRead()); 247 assertFalse(f.canWrite()); 248 assertFalse(f.canExecute()); 249 250 assertFileOwnedBy(f, "root"); 251 assertFileOwnedByGroup(f, "root"); 252 } 253 254 @MediumTest testIdletimerDirectoryExistsAndSane()255 public void testIdletimerDirectoryExistsAndSane() throws Exception { 256 File dir = new File("/sys/class/xt_idletimer"); 257 assertTrue(dir.isDirectory()); 258 assertTrue(dir.canRead()); 259 assertFalse(dir.canWrite()); 260 assertTrue(dir.canExecute()); 261 262 assertFileOwnedBy(dir, "root"); 263 assertFileOwnedByGroup(dir, "root"); 264 } 265 266 /** 267 * Assert that a file is owned by a specific owner. This is a noop if the 268 * file does not exist. 269 * 270 * @param file The file to check. 271 * @param expectedOwner The owner of the file. 272 */ assertFileOwnedBy(File file, String expectedOwner)273 private static void assertFileOwnedBy(File file, String expectedOwner) { 274 FileUtils.FileStatus status = new FileUtils.FileStatus(); 275 String path = file.getAbsolutePath(); 276 if (file.exists() && FileUtils.getFileStatus(path, status, true)) { 277 String actualOwner = FileUtils.getUserName(status.uid); 278 if (!expectedOwner.equals(actualOwner)) { 279 String msg = String.format("Wrong owner. Expected '%s', but found '%s' for %s.", 280 expectedOwner, actualOwner, path); 281 fail(msg); 282 } 283 } 284 } 285 286 /** 287 * Assert that a file is owned by a specific group. This is a noop if the 288 * file does not exist. 289 * 290 * @param file The file to check. 291 * @param expectedGroup The owner group of the file. 292 */ assertFileOwnedByGroup(File file, String expectedGroup)293 private static void assertFileOwnedByGroup(File file, String expectedGroup) { 294 FileUtils.FileStatus status = new FileUtils.FileStatus(); 295 String path = file.getAbsolutePath(); 296 if (file.exists() && FileUtils.getFileStatus(path, status, true)) { 297 String actualGroup = FileUtils.getGroupName(status.gid); 298 if (!expectedGroup.equals(actualGroup)) { 299 String msg = String.format("Wrong group. Expected '%s', but found '%s' for %s.", 300 expectedGroup, actualGroup, path); 301 fail(msg); 302 } 303 } 304 } 305 306 @MediumTest testTtyO3Sane()307 public void testTtyO3Sane() throws Exception { 308 File f = new File("/dev/ttyO3"); 309 assertFalse(f.canRead()); 310 assertFalse(f.canWrite()); 311 assertFalse(f.canExecute()); 312 } 313 314 @MediumTest testDataMediaSane()315 public void testDataMediaSane() throws Exception { 316 final File f = new File("/data/media"); 317 assertFalse(f.canRead()); 318 assertFalse(f.canWrite()); 319 assertFalse(f.canExecute()); 320 } 321 322 @MediumTest testMntShellSane()323 public void testMntShellSane() throws Exception { 324 final File f = new File("/mnt/shell"); 325 assertFalse(f.canRead()); 326 assertFalse(f.canWrite()); 327 assertFalse(f.canExecute()); 328 } 329 330 @MediumTest testMntSecureSane()331 public void testMntSecureSane() throws Exception { 332 final File f = new File("/mnt/secure"); 333 assertFalse(f.canRead()); 334 assertFalse(f.canWrite()); 335 assertFalse(f.canExecute()); 336 } 337 isDirectoryWritable(File directory)338 private static boolean isDirectoryWritable(File directory) { 339 File toCreate = new File(directory, "hello"); 340 try { 341 toCreate.createNewFile(); 342 return true; 343 } catch (IOException e) { 344 // It's expected we'll get a "Permission denied" exception. 345 } finally { 346 toCreate.delete(); 347 } 348 return false; 349 } 350 351 /** 352 * Verify that any publicly readable directories reachable from 353 * the root directory are not writable. An application should only be 354 * able to write to it's own home directory. World writable directories 355 * are a security hole because they enable a number of different attacks. 356 * <ul> 357 * <li><a href="http://en.wikipedia.org/wiki/Symlink_race">Symlink Races</a></li> 358 * <li>Data destruction by deleting or renaming files you don't own</li> 359 * <li>Data substitution by replacing trusted files with untrusted files</li> 360 * </ul> 361 * 362 * Note: Because not all directories are readable, this is a best-effort 363 * test only. Writable directories within unreadable subdirectories 364 * will NOT be detected by this code. 365 */ 366 @LargeTest testAllOtherDirectoriesNotWritable()367 public void testAllOtherDirectoriesNotWritable() throws Exception { 368 File start = new File("/"); 369 Set<File> writableDirs = getWritableDirectoryiesAndSubdirectoriesOf(start); 370 371 assertTrue("Found writable directories: " + writableDirs.toString(), 372 writableDirs.isEmpty()); 373 } 374 375 private static final Set<String> OTHER_RANDOM_DIRECTORIES = new HashSet<String>( 376 Arrays.asList( 377 "/app-cache", 378 "/app-cache/ciq/socket", 379 "/cache/fotapkg", 380 "/cache/fotapkg/tmp", 381 "/data/_SamsungBnR_", 382 "/data/_SamsungBnR_/BR", 383 "/data/2nd-init", 384 "/data/amit", 385 "/data/anr", 386 "/data/app", 387 "/data/app-private", 388 "/data/backup", 389 "/data/battd", 390 "/data/bootlogo", 391 "/data/btips", 392 "/data/btips/TI", 393 "/data/btips/TI/opp", 394 "/data/cache", 395 "/data/calibration", 396 "/data/clipboard", 397 "/data/clp", 398 "/data/dalvik-cache", 399 "/data/data", 400 "/data/data/.drm", 401 "/data/data/.drm/.wmdrm", 402 "/data/data/cw", 403 "/data/data/com.android.htcprofile", 404 "/data/data/com.android.providers.drm/rights", 405 "/data/data/com.htc.android.qxdm2sd", 406 "/data/data/com.htc.android.qxdm2sd/bin", 407 "/data/data/com.htc.android.qxdm2sd/data", 408 "/data/data/com.htc.android.qxdm2sd/tmp", 409 "/data/data/com.htc.android.netlogger/data", 410 "/data/data/com.htc.messagecs/att", 411 "/data/data/com.htc.messagecs/pdu", 412 "/data/data/com.htc.loggers/bin", 413 "/data/data/com.htc.loggers/data", 414 "/data/data/com.htc.loggers/htclog", 415 "/data/data/com.htc.loggers/tmp", 416 "/data/data/com.htc.loggers/htcghost", 417 "/data/data/com.lge.ers/android", 418 "/data/data/com.lge.ers/arm9", 419 "/data/data/com.lge.ers/kernel", 420 "/data/data/com.lge.wmc", 421 "/data/data/com.redbend.vdmc/lib", 422 "/data/data/recovery", 423 "/data/data/recovery/HTCFOTA", 424 "/data/data/recovery/OMADM", 425 "/data/data/shared", 426 "/data/diag_logs", 427 "/data/dontpanic", 428 "/data/drm", 429 "/data/drm/fwdlock", 430 "/data/drm/IDM", 431 "/data/drm/IDM/HTTP", 432 "/data/drm/rights", 433 "/data/dump", 434 "/data/efslog", 435 "/data/emt", 436 "/data/factory", 437 "/data/fics", 438 "/data/fics/dev", 439 "/data/fota", 440 "/data/gps", 441 "/data/gps/log", 442 "/data/gps/var", 443 "/data/gps/var/run", 444 "/data/gpscfg", 445 "/data/hwvefs", 446 "/data/htcfs", 447 "/data/img", 448 "/data/install", 449 "/data/internal-device", 450 "/data/internal-device/DCIM", 451 "/data/last_alog", 452 "/data/last_klog", 453 "/data/local", 454 "/data/local/logs", 455 "/data/local/logs/kernel", 456 "/data/local/logs/logcat", 457 "/data/local/logs/resetlog", 458 "/data/local/logs/smem", 459 "/data/local/mono", 460 "/data/local/mono/pulse", 461 "/data/local/purple", 462 "/data/local/purple/sound", 463 "/data/local/rights", 464 "/data/local/rwsystag", 465 "/data/local/skel", 466 "/data/local/skel/default", 467 "/data/local/skel/defualt", // Mispelled "defualt" is intentional 468 "/data/local/tmp", 469 "/data/local/tmp/com.nuance.android.vsuite.vsuiteapp", 470 "/data/log", 471 "/data/logger", 472 "/data/logs", 473 "/data/logs/core", 474 "/data/lost+found", 475 "/data/mdl", 476 "/data/misc", 477 "/data/misc/bluetooth", 478 "/data/misc/dhcp", 479 "/data/misc/lockscreen", 480 "/data/misc/webwidgets", 481 "/data/misc/webwidgets/chess", 482 "/data/misc/widgets", 483 "/data/misc/wifi", 484 "/data/misc/wifi/sockets", 485 "/data/misc/wimax", 486 "/data/misc/wimax/sockets", 487 "/data/misc/wminput", 488 "/data/misc/wpa_supplicant", 489 "/data/nv", 490 "/data/nvcam", 491 "/data/panic", 492 "/data/panicreports", 493 "/data/preinstall_md5", 494 "/data/property", 495 "/data/radio", 496 "/data/secure", 497 "/data/security", 498 "/data/sensors", 499 "/data/shared", 500 "/data/simcom", 501 "/data/simcom/btadd", 502 "/data/simcom/simlog", 503 "/data/system", 504 "/data/tmp", 505 "/data/tombstones", 506 "/data/tombstones/ramdump", 507 "/data/tpapi", 508 "/data/tpapi/etc", 509 "/data/tpapi/etc/tpa", 510 "/data/tpapi/etc/tpa/persistent", 511 "/data/tpapi/user.bin", 512 "/data/vpnch", 513 "/data/wapi", 514 "/data/wifi", 515 "/data/wimax", 516 "/data/wimax/log", 517 "/data/wiper", 518 "/data/wpstiles", 519 "/data/xt9", 520 "/dbdata/databases", 521 "/efs/.android", 522 "/mnt/sdcard", 523 "/mnt/usbdrive", 524 "/mnt_ext", 525 "/mnt_ext/badablk2", 526 "/mnt_ext/badablk3", 527 "/mnt_ext/cache", 528 "/mnt_ext/data", 529 "/system/etc/dhcpcd/dhcpcd-run-hooks", 530 "/system/etc/security/drm", 531 "/synthesis/hades", 532 "/synthesis/chimaira", 533 "/synthesis/shdisp", 534 "/synthesis/hdmi", 535 "/tmp" 536 ) 537 ); 538 539 /** 540 * Verify that directories not discoverable by 541 * testAllOtherDirectoriesNotWritable are not writable. An application 542 * should only be able to write to it's own home directory. World 543 * writable directories are a security hole because they enable a 544 * number of different attacks. 545 * <ul> 546 * <li><a href="http://en.wikipedia.org/wiki/Symlink_race">Symlink Races</a></li> 547 * <li>Data destruction by deleting or renaming files you don't own</li> 548 * <li>Data substitution by replacing trusted files with untrusted files</li> 549 * </ul> 550 * 551 * Because /data and /data/data are not readable, we blindly try to 552 * poke around in there looking for bad directories. There has to be 553 * a better way... 554 */ 555 @LargeTest testOtherRandomDirectoriesNotWritable()556 public void testOtherRandomDirectoriesNotWritable() throws Exception { 557 Set<File> writableDirs = new HashSet<File>(); 558 for (String dir : OTHER_RANDOM_DIRECTORIES) { 559 File start = new File(dir); 560 writableDirs.addAll(getWritableDirectoryiesAndSubdirectoriesOf(start)); 561 } 562 563 assertTrue("Found writable directories: " + writableDirs.toString(), 564 writableDirs.isEmpty()); 565 } 566 567 @LargeTest testReadingSysFilesDoesntFail()568 public void testReadingSysFilesDoesntFail() throws Exception { 569 ExecutorService executor = Executors.newCachedThreadPool(); 570 tryToReadFromAllIn(new File("/sys"), executor); 571 executor.shutdownNow(); 572 } 573 tryToReadFromAllIn(File dir, ExecutorService executor)574 private static void tryToReadFromAllIn(File dir, ExecutorService executor) throws IOException { 575 assertTrue(dir.isDirectory()); 576 577 if (isSymbolicLink(dir)) { 578 // don't examine symbolic links. 579 return; 580 } 581 582 File[] files = dir.listFiles(); 583 584 if (files != null) { 585 for (File f : files) { 586 if (f.isDirectory()) { 587 tryToReadFromAllIn(f, executor); 588 } else { 589 tryFileOpenRead(f, executor); 590 } 591 } 592 } 593 } 594 tryFileOpenRead(final File f, ExecutorService executor)595 private static void tryFileOpenRead(final File f, ExecutorService executor) throws IOException { 596 // Callable requires stack variables to be final. 597 Callable<Boolean> readFile = new Callable<Boolean>() { 598 @Override 599 public Boolean call() throws Exception { 600 return tryFileRead(f); 601 } 602 }; 603 604 Boolean completed = false; 605 String fileName = null; 606 Future<Boolean> future = null; 607 try { 608 fileName = f.getCanonicalPath(); 609 610 future = executor.submit(readFile); 611 612 // Block, waiting no more than set seconds. 613 completed = future.get(3, TimeUnit.SECONDS); 614 } catch (TimeoutException e) { 615 System.out.println("TIMEOUT: " + fileName); 616 } catch (InterruptedException e) { 617 System.out.println("INTERRUPTED: " + fileName); 618 } catch (ExecutionException e) { 619 System.out.println("TASK WAS ABORTED BY EXCEPTION: " + fileName); 620 } catch (IOException e) { 621 // File.getCanonicalPath() will throw this. 622 } finally { 623 if (future != null) { 624 future.cancel(true); 625 } 626 } 627 } 628 tryFileRead(File f)629 private static Boolean tryFileRead(File f) { 630 byte[] b = new byte[1024]; 631 try { 632 System.out.println("looking at " + f.getCanonicalPath()); 633 634 FileInputStream fis = new FileInputStream(f); 635 while((fis.available() != 0) && (fis.read(b) != -1)) { 636 // throw away data 637 } 638 639 fis.close(); 640 } catch (IOException e) { 641 // ignore 642 } 643 return true; 644 } 645 646 private static final Set<File> SYS_EXCEPTIONS = new HashSet<File>( 647 Arrays.asList( 648 new File("/sys/kernel/debug/tracing/trace_marker"), 649 new File("/sys/fs/selinux/member"), 650 new File("/sys/fs/selinux/user"), 651 new File("/sys/fs/selinux/relabel"), 652 new File("/sys/fs/selinux/create"), 653 new File("/sys/fs/selinux/access"), 654 new File("/sys/fs/selinux/context") 655 )); 656 657 @LargeTest testAllFilesInSysAreNotWritable()658 public void testAllFilesInSysAreNotWritable() throws Exception { 659 Set<File> writable = getAllWritableFilesInDirAndSubDir(new File("/sys")); 660 writable.removeAll(SYS_EXCEPTIONS); 661 assertTrue("Found writable: " + writable.toString(), 662 writable.isEmpty()); 663 } 664 665 private static Set<File> getAllWritableFilesInDirAndSubDir(File dir)666 getAllWritableFilesInDirAndSubDir(File dir) throws Exception { 667 assertTrue(dir.isDirectory()); 668 Set<File> retval = new HashSet<File>(); 669 670 if (isSymbolicLink(dir)) { 671 // don't examine symbolic links. 672 return retval; 673 } 674 675 File[] subDirectories = dir.listFiles(new FileFilter() { 676 @Override public boolean accept(File pathname) { 677 return pathname.isDirectory(); 678 } 679 }); 680 681 682 /* recurse into subdirectories */ 683 if (subDirectories != null) { 684 for (File f : subDirectories) { 685 retval.addAll(getAllWritableFilesInDirAndSubDir(f)); 686 } 687 } 688 689 File[] filesInThisDirectory = dir.listFiles(new FileFilter() { 690 @Override public boolean accept(File pathname) { 691 return pathname.isFile(); 692 } 693 }); 694 if (filesInThisDirectory == null) { 695 return retval; 696 } 697 698 for (File f: filesInThisDirectory) { 699 if (f.canWrite()) { 700 retval.add(f.getCanonicalFile()); 701 } 702 } 703 return retval; 704 } 705 testSystemMountedRO()706 public void testSystemMountedRO() throws IOException { 707 ParsedMounts pm = new ParsedMounts("/proc/self/mounts"); 708 String mountPoint = pm.findMountPointContaining(new File("/system")); 709 assertTrue(mountPoint + " is not mounted read-only", pm.isMountReadOnly(mountPoint)); 710 } 711 712 /** 713 * Test that the /system directory, as mounted by init, is mounted read-only. 714 * Different processes can have different mount namespaces, so init 715 * may be in a different mount namespace than Zygote spawned processes. 716 * 717 * This test assumes that init's filesystem layout is roughly identical 718 * to Zygote's filesystem layout. If this assumption ever changes, we should 719 * delete this test. 720 */ testSystemMountedRO_init()721 public void testSystemMountedRO_init() throws IOException { 722 ParsedMounts pm = new ParsedMounts("/proc/1/mounts"); 723 String mountPoint = pm.findMountPointContaining(new File("/system")); 724 assertTrue(mountPoint + " is not mounted read-only", pm.isMountReadOnly(mountPoint)); 725 } 726 testRootMountedRO()727 public void testRootMountedRO() throws IOException { 728 ParsedMounts pm = new ParsedMounts("/proc/self/mounts"); 729 String mountPoint = pm.findMountPointContaining(new File("/")); 730 assertTrue("The root directory \"" + mountPoint + "\" is not mounted read-only", 731 pm.isMountReadOnly(mountPoint)); 732 } 733 734 /** 735 * Test that the root directory, as mounted by init, is mounted read-only. 736 * Different processes can have different mount namespaces, so init 737 * may be in a different mount namespace than Zygote spawned processes. 738 * 739 * This test assumes that init's filesystem layout is roughly identical 740 * to Zygote's filesystem layout. If this assumption ever changes, we should 741 * delete this test. 742 */ testRootMountedRO_init()743 public void testRootMountedRO_init() throws IOException { 744 ParsedMounts pm = new ParsedMounts("/proc/1/mounts"); 745 String mountPoint = pm.findMountPointContaining(new File("/")); 746 assertTrue("The root directory \"" + mountPoint + "\" is not mounted read-only", 747 pm.isMountReadOnly(mountPoint)); 748 } 749 testAllBlockDevicesAreSecure()750 public void testAllBlockDevicesAreSecure() throws Exception { 751 Set<File> insecure = getAllInsecureDevicesInDirAndSubdir(new File("/dev"), FileUtils.S_IFBLK); 752 assertTrue("Found insecure block devices: " + insecure.toString(), 753 insecure.isEmpty()); 754 } 755 756 private static final Set<File> CHAR_DEV_EXCEPTIONS = new HashSet<File>( 757 Arrays.asList( 758 // All exceptions should be alphabetical and associated with a bug number. 759 new File("/dev/adsprpc-smd"), // b/11710243 760 new File("/dev/alarm"), // b/9035217 761 new File("/dev/ashmem"), 762 new File("/dev/binder"), 763 new File("/dev/card0"), // b/13159510 764 new File("/dev/dri/card0"), // b/13159510 765 new File("/dev/felica"), // b/11142586 766 new File("/dev/felica_ant"), // b/11142586 767 new File("/dev/felica_cen"), // b/11142586 768 new File("/dev/felica_pon"), // b/11142586 769 new File("/dev/felica_rfs"), // b/11142586 770 new File("/dev/felica_rws"), // b/11142586 771 new File("/dev/felica_uicc"), // b/11142586 772 new File("/dev/full"), 773 new File("/dev/galcore"), 774 new File("/dev/genlock"), // b/9035217 775 new File("/dev/graphics/galcore"), 776 new File("/dev/ion"), 777 new File("/dev/kgsl-2d0"), // b/11271533 778 new File("/dev/kgsl-2d1"), // b/11271533 779 new File("/dev/kgsl-3d0"), // b/9035217 780 new File("/dev/log/events"), // b/9035217 781 new File("/dev/log/main"), // b/9035217 782 new File("/dev/log/radio"), // b/9035217 783 new File("/dev/log/system"), // b/9035217 784 new File("/dev/mali0"), // b/9106968 785 new File("/dev/mali"), // b/11142586 786 new File("/dev/mm_interlock"), // b/12955573 787 new File("/dev/mm_isp"), // b/12955573 788 new File("/dev/mm_v3d"), // b/12955573 789 new File("/dev/msm_rotator"), // b/9035217 790 new File("/dev/null"), 791 new File("/dev/nvhost-as-gpu"), 792 new File("/dev/nvhost-ctrl"), // b/9088251 793 new File("/dev/nvhost-ctrl-gpu"), 794 new File("/dev/nvhost-dbg-gpu"), 795 new File("/dev/nvhost-gpu"), 796 new File("/dev/nvhost-gr2d"), // b/9088251 797 new File("/dev/nvhost-gr3d"), // b/9088251 798 new File("/dev/nvhost-tsec"), 799 new File("/dev/nvhost-prof-gpu"), 800 new File("/dev/nvhost-vic"), 801 new File("/dev/nvmap"), // b/9088251 802 new File("/dev/ptmx"), // b/9088251 803 new File("/dev/pvrsrvkm"), // b/9108170 804 new File("/dev/pvr_sync"), 805 new File("/dev/quadd"), 806 new File("/dev/random"), 807 new File("/dev/snfc_cen"), // b/11142586 808 new File("/dev/snfc_hsel"), // b/11142586 809 new File("/dev/snfc_intu_poll"), // b/11142586 810 new File("/dev/snfc_rfs"), // b/11142586 811 new File("/dev/tegra-throughput"), 812 new File("/dev/tiler"), // b/9108170 813 new File("/dev/tty"), 814 new File("/dev/urandom"), 815 new File("/dev/ump"), // b/11142586 816 new File("/dev/xt_qtaguid"), // b/9088251 817 new File("/dev/zero"), 818 new File("/dev/fimg2d"), // b/10428016 819 new File("/dev/mobicore-user") // b/10428016 820 )); 821 testAllCharacterDevicesAreSecure()822 public void testAllCharacterDevicesAreSecure() throws Exception { 823 Set<File> insecure = getAllInsecureDevicesInDirAndSubdir(new File("/dev"), FileUtils.S_IFCHR); 824 Set<File> insecurePts = getAllInsecureDevicesInDirAndSubdir(new File("/dev/pts"), FileUtils.S_IFCHR); 825 insecure.removeAll(CHAR_DEV_EXCEPTIONS); 826 insecure.removeAll(insecurePts); 827 assertTrue("Found insecure character devices: " + insecure.toString(), 828 insecure.isEmpty()); 829 } 830 testDevRandomWorldReadableAndWritable()831 public void testDevRandomWorldReadableAndWritable() throws Exception { 832 File f = new File("/dev/random"); 833 834 assertTrue(f + " cannot be opened for reading", canOpenForReading(f)); 835 assertTrue(f + " cannot be opened for writing", canOpenForWriting(f)); 836 837 FileUtils.FileStatus status = new FileUtils.FileStatus(); 838 assertTrue(FileUtils.getFileStatus(f.getPath(), status, false)); 839 assertTrue( 840 f + " not world-readable/writable. Actual mode: 0" 841 + Integer.toString(status.mode, 8), 842 (status.mode & 0666) == 0666); 843 } 844 testDevUrandomWorldReadableAndWritable()845 public void testDevUrandomWorldReadableAndWritable() throws Exception { 846 File f = new File("/dev/urandom"); 847 848 assertTrue(f + " cannot be opened for reading", canOpenForReading(f)); 849 assertTrue(f + " cannot be opened for writing", canOpenForWriting(f)); 850 851 FileUtils.FileStatus status = new FileUtils.FileStatus(); 852 assertTrue(FileUtils.getFileStatus(f.getPath(), status, false)); 853 assertTrue( 854 f + " not world-readable/writable. Actual mode: 0" 855 + Integer.toString(status.mode, 8), 856 (status.mode & 0666) == 0666); 857 } 858 testDevHwRandomLockedDown()859 public void testDevHwRandomLockedDown() throws Exception { 860 File f = new File("/dev/hw_random"); 861 if (!f.exists()) { 862 // HW RNG is not required to be exposed on all devices. 863 return; 864 } 865 866 assertFalse(f + " can be opened for reading", canOpenForReading(f)); 867 assertFalse(f + " can be opened for writing", canOpenForWriting(f)); 868 869 FileUtils.FileStatus status = new FileUtils.FileStatus(); 870 assertFalse("stat permitted on " + f, 871 FileUtils.getFileStatus(f.getPath(), status, false)); 872 } 873 canOpenForReading(File f)874 private static boolean canOpenForReading(File f) { 875 try (InputStream in = new FileInputStream(f)) { 876 return true; 877 } catch (IOException expected) { 878 return false; 879 } 880 } 881 canOpenForWriting(File f)882 private static boolean canOpenForWriting(File f) { 883 try (OutputStream out = new FileOutputStream(f)) { 884 return true; 885 } catch (IOException expected) { 886 return false; 887 } 888 } 889 testFileHasOnlyCapsThrowsOnInvalidCaps()890 public void testFileHasOnlyCapsThrowsOnInvalidCaps() throws Exception { 891 try { 892 // Ensure negative cap id fails. 893 new FileUtils.CapabilitySet() 894 .add(-1) 895 .fileHasOnly("/system/bin/run-as"); 896 fail(); 897 } 898 catch (IllegalArgumentException e) { 899 // expected 900 } 901 902 try { 903 // Ensure too-large cap throws. 904 new FileUtils.CapabilitySet() 905 .add(OsConstants.CAP_LAST_CAP + 1) 906 .fileHasOnly("/system/bin/run-as"); 907 fail(); 908 } 909 catch (IllegalArgumentException e) { 910 // expected 911 } 912 } 913 914 /** 915 * Test that the /system/bin/run-as command has setuid and setgid 916 * attributes set on the file. If these calls fail, debugger 917 * breakpoints for native code will not work as run-as will not 918 * be able to perform required elevated-privilege functionality. 919 */ testRunAsHasCorrectCapabilities()920 public void testRunAsHasCorrectCapabilities() throws Exception { 921 // ensure file is user and group read/executable 922 String filename = "/system/bin/run-as"; 923 FileUtils.FileStatus status = new FileUtils.FileStatus(); 924 assertTrue(FileUtils.getFileStatus(filename, status, false)); 925 assertTrue(status.hasModeFlag(FileUtils.S_IRUSR | FileUtils.S_IXUSR)); 926 assertTrue(status.hasModeFlag(FileUtils.S_IRGRP | FileUtils.S_IXGRP)); 927 928 // ensure file owner/group is set correctly 929 File f = new File(filename); 930 assertFileOwnedBy(f, "root"); 931 assertFileOwnedByGroup(f, "shell"); 932 933 // ensure file has setuid/setgid enabled 934 assertTrue(FileUtils.hasSetUidCapability(filename)); 935 assertTrue(FileUtils.hasSetGidCapability(filename)); 936 937 // ensure file has *only* setuid/setgid attributes enabled 938 assertTrue(new FileUtils.CapabilitySet() 939 .add(OsConstants.CAP_SETUID) 940 .add(OsConstants.CAP_SETGID) 941 .fileHasOnly("/system/bin/run-as")); 942 } 943 944 private static Set<File> getAllInsecureDevicesInDirAndSubdir(File dir, int type)945 getAllInsecureDevicesInDirAndSubdir(File dir, int type) throws Exception { 946 assertTrue(dir.isDirectory()); 947 Set<File> retval = new HashSet<File>(); 948 949 if (isSymbolicLink(dir)) { 950 // don't examine symbolic links. 951 return retval; 952 } 953 954 File[] subDirectories = dir.listFiles(new FileFilter() { 955 @Override public boolean accept(File pathname) { 956 return pathname.isDirectory(); 957 } 958 }); 959 960 961 /* recurse into subdirectories */ 962 if (subDirectories != null) { 963 for (File f : subDirectories) { 964 retval.addAll(getAllInsecureDevicesInDirAndSubdir(f, type)); 965 } 966 } 967 968 File[] filesInThisDirectory = dir.listFiles(); 969 if (filesInThisDirectory == null) { 970 return retval; 971 } 972 973 for (File f: filesInThisDirectory) { 974 FileUtils.FileStatus status = new FileUtils.FileStatus(); 975 FileUtils.getFileStatus(f.getAbsolutePath(), status, false); 976 if (status.isOfType(type)) { 977 if (f.canRead() || f.canWrite() || f.canExecute()) { 978 retval.add(f); 979 } 980 if (status.uid == 2000) { 981 // The shell user should not own any devices 982 retval.add(f); 983 } 984 985 // Don't allow devices owned by GIDs 986 // accessible to non-privileged applications. 987 if ((status.gid == 1007) // AID_LOG 988 || (status.gid == 1015) // AID_SDCARD_RW 989 || (status.gid == 1023) // AID_MEDIA_RW 990 || (status.gid == 1028) // AID_SDCARD_R 991 || (status.gid == 2000)) // AID_SHELL 992 { 993 if (status.hasModeFlag(FileUtils.S_IRGRP) 994 || status.hasModeFlag(FileUtils.S_IWGRP) 995 || status.hasModeFlag(FileUtils.S_IXGRP)) 996 { 997 retval.add(f); 998 } 999 } 1000 } 1001 } 1002 return retval; 1003 } 1004 getWritableDirectoryiesAndSubdirectoriesOf(File dir)1005 private Set<File> getWritableDirectoryiesAndSubdirectoriesOf(File dir) throws Exception { 1006 Set<File> retval = new HashSet<File>(); 1007 if (!dir.isDirectory()) { 1008 return retval; 1009 } 1010 1011 if (isSymbolicLink(dir)) { 1012 // don't examine symbolic links. 1013 return retval; 1014 } 1015 1016 String myHome = getContext().getApplicationInfo().dataDir; 1017 String thisDir = dir.getCanonicalPath(); 1018 if (thisDir.startsWith(myHome)) { 1019 // Don't examine directories within our home directory. 1020 // We expect these directories to be writable. 1021 return retval; 1022 } 1023 1024 if (isDirectoryWritable(dir)) { 1025 retval.add(dir); 1026 } 1027 1028 File[] subFiles = dir.listFiles(); 1029 if (subFiles == null) { 1030 return retval; 1031 } 1032 1033 for (File f : subFiles) { 1034 retval.addAll(getWritableDirectoryiesAndSubdirectoriesOf(f)); 1035 } 1036 1037 return retval; 1038 } 1039 isSymbolicLink(File f)1040 private static boolean isSymbolicLink(File f) throws IOException { 1041 return !f.getAbsolutePath().equals(f.getCanonicalPath()); 1042 } 1043 1044 private static class ParsedMounts { 1045 private HashMap<String, Boolean> mFileReadOnlyMap = new HashMap<String, Boolean>(); 1046 ParsedMounts(String filename)1047 private ParsedMounts(String filename) throws IOException { 1048 BufferedReader br = new BufferedReader(new FileReader(filename)); 1049 try { 1050 String line; 1051 while ((line = br.readLine()) != null) { 1052 String[] fields = line.split(" "); 1053 String mountPoint = fields[1]; 1054 String all_options = fields[3]; 1055 String[] options = all_options.split(","); 1056 boolean foundRo = false; 1057 for (String option : options) { 1058 if ("ro".equals(option)) { 1059 foundRo = true; 1060 break; 1061 } 1062 } 1063 mFileReadOnlyMap.put(mountPoint, foundRo); 1064 } 1065 } finally { 1066 br.close(); 1067 } 1068 } 1069 isMountReadOnly(String s)1070 private boolean isMountReadOnly(String s) { 1071 return mFileReadOnlyMap.get(s).booleanValue(); 1072 } 1073 findMountPointContaining(File f)1074 private String findMountPointContaining(File f) throws IOException { 1075 while (f != null) { 1076 f = f.getCanonicalFile(); 1077 String path = f.getPath(); 1078 if (mFileReadOnlyMap.containsKey(path)) { 1079 return path; 1080 } 1081 f = f.getParentFile(); 1082 } 1083 // This should NEVER be reached, as we'll eventually hit the 1084 // root directory. 1085 throw new AssertionError("Unable to find mount point"); 1086 } 1087 } 1088 } 1089