1 /* 2 * Copyright (C) 2021 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 com.android.virt.fs; 18 19 import static com.android.microdroid.test.host.CommandResultSubject.assertThat; 20 21 import static com.google.common.truth.Truth.assertThat; 22 23 import static org.junit.Assert.assertEquals; 24 import static org.junit.Assume.assumeTrue; 25 26 import android.platform.test.annotations.RootPermissionTest; 27 28 import com.android.fs.common.AuthFsTestRule; 29 import com.android.microdroid.test.host.CommandRunner; 30 import com.android.tradefed.device.DeviceNotAvailableException; 31 import com.android.tradefed.invoker.TestInformation; 32 import com.android.tradefed.log.LogUtil.CLog; 33 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; 34 import com.android.tradefed.testtype.junit4.AfterClassWithInfo; 35 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; 36 import com.android.tradefed.testtype.junit4.BeforeClassWithInfo; 37 import com.android.tradefed.util.CommandResult; 38 39 import org.junit.Rule; 40 import org.junit.Test; 41 import org.junit.runner.RunWith; 42 43 @RootPermissionTest 44 @RunWith(DeviceJUnit4ClassRunner.class) 45 public final class AuthFsHostTest extends BaseHostJUnit4Test { 46 47 /** Test directory on Android where data are located */ 48 private static final String TEST_DIR = AuthFsTestRule.TEST_DIR; 49 50 /** Output directory where the test can generate output on Android */ 51 private static final String TEST_OUTPUT_DIR = AuthFsTestRule.TEST_OUTPUT_DIR; 52 53 /** Path to fsverity on Android */ 54 private static final String FSVERITY_BIN = "/data/local/tmp/fsverity"; 55 56 /** Mount point of authfs on Microdroid during the test */ 57 private static final String MOUNT_DIR = AuthFsTestRule.MOUNT_DIR; 58 59 /** Input manifest path in the VM. */ 60 private static final String INPUT_MANIFEST_PATH = "/mnt/apk/assets/input_manifest.pb"; 61 62 // fs-verity digest (sha256) of testdata/input.{4k, 4k1, 4m} 63 private static final String DIGEST_4K = 64 "sha256-9828cd65f4744d6adda216d3a63d8205375be485bfa261b3b8153d3358f5a576"; 65 private static final String DIGEST_4K1 = 66 "sha256-3c70dcd4685ed256ebf1ef116c12e472f35b5017eaca422c0483dadd7d0b5a9f"; 67 private static final String DIGEST_4M = 68 "sha256-f18a268d565348fb4bbf11f10480b198f98f2922eb711de149857b3cecf98a8d"; 69 70 private static CommandRunner sAndroid; 71 private static CommandRunner sMicrodroid; 72 73 @Rule public final AuthFsTestRule mAuthFsTestRule = new AuthFsTestRule(); 74 75 @BeforeClassWithInfo beforeClassWithDevice(TestInformation testInfo)76 public static void beforeClassWithDevice(TestInformation testInfo) throws Exception { 77 AuthFsTestRule.setUpAndroid(testInfo); 78 assumeTrue(AuthFsTestRule.getDevice().supportsMicrodroid(/*protectedVm=*/ true)); 79 AuthFsTestRule.startMicrodroid(/*protectedVm=*/ true); 80 sAndroid = AuthFsTestRule.getAndroid(); 81 sMicrodroid = AuthFsTestRule.getMicrodroid(); 82 } 83 84 @AfterClassWithInfo afterClassWithDevice(TestInformation testInfo)85 public static void afterClassWithDevice(TestInformation testInfo) 86 throws DeviceNotAvailableException { 87 AuthFsTestRule.shutdownMicrodroid(); 88 AuthFsTestRule.tearDownAndroid(); 89 } 90 91 @Test testReadWithFsverityVerification_RemoteFile()92 public void testReadWithFsverityVerification_RemoteFile() throws Exception { 93 // Setup 94 runFdServerOnAndroid( 95 "--open-ro 3:input.4m --open-ro 4:input.4m.fsv_meta --open-ro 6:input.4m", 96 "--ro-fds 3:4 --ro-fds 6"); 97 runAuthFsOnMicrodroid("--remote-ro-file-unverified 6 --remote-ro-file 3:" + DIGEST_4M); 98 99 // Action 100 String actualHashUnverified4m = computeFileHash(sMicrodroid, MOUNT_DIR + "/6"); 101 String actualHash4m = computeFileHash(sMicrodroid, MOUNT_DIR + "/3"); 102 103 // Verify 104 String expectedHash4m = computeFileHash(sAndroid, TEST_DIR + "/input.4m"); 105 106 assertEquals("Inconsistent hash from /authfs/6: ", expectedHash4m, actualHashUnverified4m); 107 assertEquals("Inconsistent hash from /authfs/3: ", expectedHash4m, actualHash4m); 108 } 109 110 // Separate the test from the above simply because exec in shell does not allow open too many 111 // files. 112 @Test testReadWithFsverityVerification_RemoteSmallerFile()113 public void testReadWithFsverityVerification_RemoteSmallerFile() throws Exception { 114 // Setup 115 runFdServerOnAndroid( 116 "--open-ro 3:input.4k --open-ro 4:input.4k.fsv_meta --open-ro" 117 + " 6:input.4k1 --open-ro 7:input.4k1.fsv_meta", 118 "--ro-fds 3:4 --ro-fds 6:7"); 119 runAuthFsOnMicrodroid( 120 "--remote-ro-file 3:" + DIGEST_4K + " --remote-ro-file 6:" + DIGEST_4K1); 121 122 // Action 123 String actualHash4k = computeFileHash(sMicrodroid, MOUNT_DIR + "/3"); 124 String actualHash4k1 = computeFileHash(sMicrodroid, MOUNT_DIR + "/6"); 125 126 // Verify 127 String expectedHash4k = computeFileHash(sAndroid, TEST_DIR + "/input.4k"); 128 String expectedHash4k1 = computeFileHash(sAndroid, TEST_DIR + "/input.4k1"); 129 130 assertEquals("Inconsistent hash from /authfs/3: ", expectedHash4k, actualHash4k); 131 assertEquals("Inconsistent hash from /authfs/6: ", expectedHash4k1, actualHash4k1); 132 } 133 134 @Test testReadWithFsverityVerification_TamperedMerkleTree()135 public void testReadWithFsverityVerification_TamperedMerkleTree() throws Exception { 136 // Setup 137 runFdServerOnAndroid( 138 "--open-ro 3:input.4m --open-ro 4:input.4m.fsv_meta.bad_merkle", 139 "--ro-fds 3:4"); 140 runAuthFsOnMicrodroid("--remote-ro-file 3:" + DIGEST_4M); 141 142 // Verify 143 assertThat(copyFile(sMicrodroid, MOUNT_DIR + "/3", "/dev/null")).isFailed(); 144 } 145 146 @Test testReadWithFsverityVerification_FdServerUsesRealFsverityData()147 public void testReadWithFsverityVerification_FdServerUsesRealFsverityData() throws Exception { 148 // Setup (fs-verity is enabled for input.file in AndroidTest.xml) 149 runFdServerOnAndroid("--open-ro 3:input.file", "--ro-fds 3"); 150 String expectedDigest = 151 sAndroid.run(FSVERITY_BIN + " digest --compact " + TEST_DIR + "/input.file"); 152 runAuthFsOnMicrodroid("--remote-ro-file 3:sha256-" + expectedDigest); 153 154 // Action 155 String actualHash = computeFileHash(sMicrodroid, MOUNT_DIR + "/3"); 156 157 // Verify 158 String expectedHash = computeFileHash(sAndroid, TEST_DIR + "/input.file"); 159 assertEquals("Inconsistent hash from /authfs/3: ", expectedHash, actualHash); 160 } 161 162 @Test testWriteThroughCorrectly()163 public void testWriteThroughCorrectly() throws Exception { 164 // Setup 165 runFdServerOnAndroid("--open-rw 3:" + TEST_OUTPUT_DIR + "/out.file", "--rw-fds 3"); 166 runAuthFsOnMicrodroid("--remote-new-rw-file 3"); 167 168 // Action 169 String srcPath = "/system/bin/linker64"; 170 String destPath = MOUNT_DIR + "/3"; 171 String backendPath = TEST_OUTPUT_DIR + "/out.file"; 172 assertThat(copyFile(sMicrodroid, srcPath, destPath)).isSuccess(); 173 174 // Verify 175 String expectedHash = computeFileHash(sMicrodroid, srcPath); 176 expectBackingFileConsistency(destPath, backendPath, expectedHash); 177 } 178 179 @Test testWriteFailedIfDetectsTampering()180 public void testWriteFailedIfDetectsTampering() throws Exception { 181 // Setup 182 runFdServerOnAndroid("--open-rw 3:" + TEST_OUTPUT_DIR + "/out.file", "--rw-fds 3"); 183 runAuthFsOnMicrodroid("--remote-new-rw-file 3"); 184 185 String srcPath = "/system/bin/linker64"; 186 String destPath = MOUNT_DIR + "/3"; 187 String backendPath = TEST_OUTPUT_DIR + "/out.file"; 188 assertThat(copyFile(sMicrodroid, srcPath, destPath)).isSuccess(); 189 190 // Action 191 // Tampering with the first 2 4K-blocks of the backing file. 192 assertThat( 193 writeZerosAtFileOffset(sAndroid, backendPath, 194 /* offset */ 0, /* number */ 8192, /* writeThrough */ false)) 195 .isSuccess(); 196 197 // Verify 198 // Write to a block partially requires a read back to calculate the new hash. It should fail 199 // when the content is inconsistent to the known hash. Use direct I/O to avoid simply 200 // writing to the filesystem cache. 201 assertThat( 202 writeZerosAtFileOffset(sMicrodroid, destPath, 203 /* offset */ 0, /* number */ 1024, /* writeThrough */ true)) 204 .isFailed(); 205 206 // A full 4K write does not require to read back, so write can succeed even if the backing 207 // block has already been tampered. 208 assertThat( 209 writeZerosAtFileOffset(sMicrodroid, destPath, 210 /* offset */ 4096, /* number */ 4096, /* writeThrough */ false)) 211 .isSuccess(); 212 213 // Otherwise, a partial write with correct backing file should still succeed. 214 assertThat( 215 writeZerosAtFileOffset(sMicrodroid, destPath, 216 /* offset */ 8192, /* number */ 1024, /* writeThrough */ false)) 217 .isSuccess(); 218 } 219 220 @Test testReadFailedIfDetectsTampering()221 public void testReadFailedIfDetectsTampering() throws Exception { 222 // Setup 223 runFdServerOnAndroid("--open-rw 3:" + TEST_OUTPUT_DIR + "/out.file", "--rw-fds 3"); 224 runAuthFsOnMicrodroid("--remote-new-rw-file 3"); 225 226 String srcPath = "/system/bin/linker64"; 227 String destPath = MOUNT_DIR + "/3"; 228 String backendPath = TEST_OUTPUT_DIR + "/out.file"; 229 assertThat(copyFile(sMicrodroid, srcPath, destPath)).isSuccess(); 230 231 // Action 232 // Tampering with the first 4K-block of the backing file. 233 assertThat( 234 writeZerosAtFileOffset(sAndroid, backendPath, 235 /* offset */ 0, /* number */ 4096, /* writeThrough */ false)) 236 .isSuccess(); 237 238 // Verify 239 // Force dropping the page cache, so that the next read can be validated. 240 sMicrodroid.run("echo 1 > /proc/sys/vm/drop_caches"); 241 // A read will fail if the backing data has been tampered. 242 assertThat(checkReadAt(sMicrodroid, destPath, /* offset */ 0, /* number */ 4096)) 243 .isFailed(); 244 assertThat(checkReadAt(sMicrodroid, destPath, /* offset */ 4096, /* number */ 4096)) 245 .isSuccess(); 246 } 247 248 @Test testResizeFailedIfDetectsTampering()249 public void testResizeFailedIfDetectsTampering() throws Exception { 250 // Setup 251 runFdServerOnAndroid("--open-rw 3:" + TEST_OUTPUT_DIR + "/out.file", "--rw-fds 3"); 252 runAuthFsOnMicrodroid("--remote-new-rw-file 3"); 253 254 String outputPath = MOUNT_DIR + "/3"; 255 String backendPath = TEST_OUTPUT_DIR + "/out.file"; 256 createFileWithOnes(sMicrodroid, outputPath, 8192); 257 258 // Action 259 // Tampering with the last 4K-block of the backing file. 260 assertThat( 261 writeZerosAtFileOffset(sAndroid, backendPath, 262 /* offset */ 4096, /* number */ 1, /* writeThrough */ false)) 263 .isSuccess(); 264 265 // Verify 266 // A resize (to a non-multiple of 4K) will fail if the last backing chunk has been 267 // tampered. The original data is necessary (and has to be verified) to calculate the new 268 // hash with shorter data. 269 assertThat(resizeFile(sMicrodroid, outputPath, 8000)).isFailed(); 270 } 271 272 @Test testFileResize()273 public void testFileResize() throws Exception { 274 // Setup 275 runFdServerOnAndroid("--open-rw 3:" + TEST_OUTPUT_DIR + "/out.file", "--rw-fds 3"); 276 runAuthFsOnMicrodroid("--remote-new-rw-file 3"); 277 String outputPath = MOUNT_DIR + "/3"; 278 String backendPath = TEST_OUTPUT_DIR + "/out.file"; 279 280 // Action & Verify 281 createFileWithOnes(sMicrodroid, outputPath, 10000); 282 assertEquals(getFileSizeInBytes(sMicrodroid, outputPath), 10000); 283 expectBackingFileConsistency( 284 outputPath, 285 backendPath, 286 "684ad25fdc2bbb80cbc910dd1bde6d5499ccf860ca6ee44704b77ec445271353"); 287 288 assertThat(resizeFile(sMicrodroid, outputPath, 15000)).isSuccess(); 289 assertEquals(getFileSizeInBytes(sMicrodroid, outputPath), 15000); 290 expectBackingFileConsistency( 291 outputPath, 292 backendPath, 293 "567c89f62586e0d33369157afdfe99a2fa36cdffb01e91dcdc0b7355262d610d"); 294 295 assertThat(resizeFile(sMicrodroid, outputPath, 5000)).isSuccess(); 296 assertEquals(getFileSizeInBytes(sMicrodroid, outputPath), 5000); 297 expectBackingFileConsistency( 298 outputPath, 299 backendPath, 300 "e53130831c13dabff71d5d1797e3aaa467b4b7d32b3b8782c4ff03d76976f2aa"); 301 } 302 303 @Test testOutputDirectory_WriteNewFiles()304 public void testOutputDirectory_WriteNewFiles() throws Exception { 305 // Setup 306 String androidOutputDir = TEST_OUTPUT_DIR + "/dir"; 307 String authfsOutputDir = MOUNT_DIR + "/3"; 308 sAndroid.run("mkdir " + androidOutputDir); 309 runFdServerOnAndroid("--open-dir 3:" + androidOutputDir, "--rw-dirs 3"); 310 runAuthFsOnMicrodroid("--remote-new-rw-dir 3"); 311 312 // Action & Verify 313 // Can create a new file to write. 314 String expectedAndroidPath = androidOutputDir + "/file"; 315 String authfsPath = authfsOutputDir + "/file"; 316 createFileWithOnes(sMicrodroid, authfsPath, 10000); 317 assertEquals(getFileSizeInBytes(sMicrodroid, authfsPath), 10000); 318 expectBackingFileConsistency( 319 authfsPath, 320 expectedAndroidPath, 321 "684ad25fdc2bbb80cbc910dd1bde6d5499ccf860ca6ee44704b77ec445271353"); 322 323 // Regular file operations work, e.g. resize. 324 assertThat(resizeFile(sMicrodroid, authfsPath, 15000)).isSuccess(); 325 assertEquals(getFileSizeInBytes(sMicrodroid, authfsPath), 15000); 326 expectBackingFileConsistency( 327 authfsPath, 328 expectedAndroidPath, 329 "567c89f62586e0d33369157afdfe99a2fa36cdffb01e91dcdc0b7355262d610d"); 330 } 331 332 @Test testOutputDirectory_MkdirAndWriteFile()333 public void testOutputDirectory_MkdirAndWriteFile() throws Exception { 334 // Setup 335 String androidOutputDir = TEST_OUTPUT_DIR + "/dir"; 336 String authfsOutputDir = MOUNT_DIR + "/3"; 337 sAndroid.run("mkdir " + androidOutputDir); 338 runFdServerOnAndroid("--open-dir 3:" + androidOutputDir, "--rw-dirs 3"); 339 runAuthFsOnMicrodroid("--remote-new-rw-dir 3"); 340 341 // Action 342 // Can create nested directories and can create a file in one. 343 sMicrodroid.run("mkdir " + authfsOutputDir + "/new_dir"); 344 sMicrodroid.run("mkdir -p " + authfsOutputDir + "/we/need/to/go/deeper"); 345 createFileWithOnes(sMicrodroid, authfsOutputDir + "/new_dir/file1", 10000); 346 createFileWithOnes(sMicrodroid, authfsOutputDir + "/we/need/file2", 10000); 347 348 // Verify 349 // Directories show up in Android. 350 sAndroid.run("test -d " + androidOutputDir + "/new_dir"); 351 sAndroid.run("test -d " + androidOutputDir + "/we/need/to/go/deeper"); 352 // Files exist in Android. Hashes on Microdroid and Android are consistent. 353 assertEquals(getFileSizeInBytes(sMicrodroid, authfsOutputDir + "/new_dir/file1"), 10000); 354 expectBackingFileConsistency( 355 authfsOutputDir + "/new_dir/file1", 356 androidOutputDir + "/new_dir/file1", 357 "684ad25fdc2bbb80cbc910dd1bde6d5499ccf860ca6ee44704b77ec445271353"); 358 // Same to file in a nested directory. 359 assertEquals(getFileSizeInBytes(sMicrodroid, authfsOutputDir + "/we/need/file2"), 10000); 360 expectBackingFileConsistency( 361 authfsOutputDir + "/we/need/file2", 362 androidOutputDir + "/we/need/file2", 363 "684ad25fdc2bbb80cbc910dd1bde6d5499ccf860ca6ee44704b77ec445271353"); 364 } 365 366 @Test testOutputDirectory_CreateAndTruncateExistingFile()367 public void testOutputDirectory_CreateAndTruncateExistingFile() throws Exception { 368 // Setup 369 String androidOutputDir = TEST_OUTPUT_DIR + "/dir"; 370 String authfsOutputDir = MOUNT_DIR + "/3"; 371 sAndroid.run("mkdir " + androidOutputDir); 372 runFdServerOnAndroid("--open-dir 3:" + androidOutputDir, "--rw-dirs 3"); 373 runAuthFsOnMicrodroid("--remote-new-rw-dir 3"); 374 375 // Action & Verify 376 sMicrodroid.run("echo -n foo > " + authfsOutputDir + "/file"); 377 assertEquals(getFileSizeInBytes(sMicrodroid, authfsOutputDir + "/file"), 3); 378 // Can override a file and write normally. 379 createFileWithOnes(sMicrodroid, authfsOutputDir + "/file", 10000); 380 assertEquals(getFileSizeInBytes(sMicrodroid, authfsOutputDir + "/file"), 10000); 381 expectBackingFileConsistency( 382 authfsOutputDir + "/file", 383 androidOutputDir + "/file", 384 "684ad25fdc2bbb80cbc910dd1bde6d5499ccf860ca6ee44704b77ec445271353"); 385 } 386 387 @Test testOutputDirectory_CanDeleteFile()388 public void testOutputDirectory_CanDeleteFile() throws Exception { 389 // Setup 390 String androidOutputDir = TEST_OUTPUT_DIR + "/dir"; 391 String authfsOutputDir = MOUNT_DIR + "/3"; 392 sAndroid.run("mkdir " + androidOutputDir); 393 runFdServerOnAndroid("--open-dir 3:" + androidOutputDir, "--rw-dirs 3"); 394 runAuthFsOnMicrodroid("--remote-new-rw-dir 3"); 395 396 sMicrodroid.run("echo -n foo > " + authfsOutputDir + "/file"); 397 sMicrodroid.run("test -f " + authfsOutputDir + "/file"); 398 sAndroid.run("test -f " + androidOutputDir + "/file"); 399 400 // Action & Verify 401 sMicrodroid.run("rm " + authfsOutputDir + "/file"); 402 sMicrodroid.run("test ! -f " + authfsOutputDir + "/file"); 403 sAndroid.run("test ! -f " + androidOutputDir + "/file"); 404 } 405 406 @Test testOutputDirectory_CanDeleteDirectoryOnlyIfEmpty()407 public void testOutputDirectory_CanDeleteDirectoryOnlyIfEmpty() throws Exception { 408 // Setup 409 String androidOutputDir = TEST_OUTPUT_DIR + "/dir"; 410 String authfsOutputDir = MOUNT_DIR + "/3"; 411 sAndroid.run("mkdir " + androidOutputDir); 412 runFdServerOnAndroid("--open-dir 3:" + androidOutputDir, "--rw-dirs 3"); 413 runAuthFsOnMicrodroid("--remote-new-rw-dir 3"); 414 415 sMicrodroid.run("mkdir -p " + authfsOutputDir + "/dir/dir2"); 416 sMicrodroid.run("echo -n foo > " + authfsOutputDir + "/dir/file"); 417 sAndroid.run("test -d " + androidOutputDir + "/dir/dir2"); 418 419 // Action & Verify 420 sMicrodroid.run("rmdir " + authfsOutputDir + "/dir/dir2"); 421 sMicrodroid.run("test ! -d " + authfsOutputDir + "/dir/dir2"); 422 sAndroid.run("test ! -d " + androidOutputDir + "/dir/dir2"); 423 // Can only delete a directory if empty 424 assertThat(sMicrodroid.runForResult("rmdir " + authfsOutputDir + "/dir")).isFailed(); 425 sMicrodroid.run("test -d " + authfsOutputDir + "/dir"); // still there 426 sMicrodroid.run("rm " + authfsOutputDir + "/dir/file"); 427 sMicrodroid.run("rmdir " + authfsOutputDir + "/dir"); 428 sMicrodroid.run("test ! -d " + authfsOutputDir + "/dir"); 429 sAndroid.run("test ! -d " + androidOutputDir + "/dir"); 430 } 431 432 @Test testOutputDirectory_CannotRecreateDirectoryIfNameExists()433 public void testOutputDirectory_CannotRecreateDirectoryIfNameExists() throws Exception { 434 // Setup 435 String androidOutputDir = TEST_OUTPUT_DIR + "/dir"; 436 String authfsOutputDir = MOUNT_DIR + "/3"; 437 sAndroid.run("mkdir " + androidOutputDir); 438 runFdServerOnAndroid("--open-dir 3:" + androidOutputDir, "--rw-dirs 3"); 439 runAuthFsOnMicrodroid("--remote-new-rw-dir 3"); 440 441 sMicrodroid.run("touch " + authfsOutputDir + "/some_file"); 442 sMicrodroid.run("mkdir " + authfsOutputDir + "/some_dir"); 443 sMicrodroid.run("touch " + authfsOutputDir + "/some_dir/file"); 444 sMicrodroid.run("mkdir " + authfsOutputDir + "/some_dir/dir"); 445 446 // Action & Verify 447 // Cannot create directory if an entry with the same name already exists. 448 assertThat(sMicrodroid.runForResult("mkdir " + authfsOutputDir + "/some_file")).isFailed(); 449 assertThat(sMicrodroid.runForResult("mkdir " + authfsOutputDir + "/some_dir")).isFailed(); 450 assertThat(sMicrodroid.runForResult("mkdir " + authfsOutputDir + "/some_dir/file")) 451 .isFailed(); 452 assertThat(sMicrodroid.runForResult("mkdir " + authfsOutputDir + "/some_dir/dir")) 453 .isFailed(); 454 } 455 456 @Test testOutputDirectory_WriteToFdOfDeletedFile()457 public void testOutputDirectory_WriteToFdOfDeletedFile() throws Exception { 458 // Setup 459 String authfsOutputDir = MOUNT_DIR + "/3"; 460 String androidOutputDir = TEST_OUTPUT_DIR + "/dir"; 461 sAndroid.run("mkdir " + androidOutputDir); 462 runFdServerOnAndroid("--open-dir 3:" + androidOutputDir, "--rw-dirs 3"); 463 runAuthFsOnMicrodroid("--remote-new-rw-dir 3"); 464 465 // Create a file with some data. Test the existence. 466 String outputPath = authfsOutputDir + "/out"; 467 String androidOutputPath = androidOutputDir + "/out"; 468 sMicrodroid.run("echo -n 123 > " + outputPath); 469 sMicrodroid.run("test -f " + outputPath); 470 sAndroid.run("test -f " + androidOutputPath); 471 472 // Action 473 String output = sMicrodroid.run( 474 // Open the file for append and read 475 "exec 4>>" + outputPath + " 5<" + outputPath + "; " 476 // Delete the file from the directory 477 + "rm " + outputPath + "; " 478 // Append more data to the file descriptor 479 + "echo -n 456 >&4; " 480 // Print the whole file from the file descriptor 481 + "cat <&5"); 482 483 // Verify 484 // Output contains all written data, while the files are deleted. 485 assertEquals("123456", output); 486 sMicrodroid.run("test ! -f " + outputPath); 487 sAndroid.run("test ! -f " + androidOutputDir + "/out"); 488 } 489 490 @Test testInputDirectory_CanReadFile()491 public void testInputDirectory_CanReadFile() throws Exception { 492 // Setup 493 String authfsInputDir = MOUNT_DIR + "/3"; 494 runFdServerOnAndroid("--open-dir 3:" + TEST_DIR, "--ro-dirs 3"); 495 runAuthFsOnMicrodroid("--remote-ro-dir 3:" + INPUT_MANIFEST_PATH + ":"); 496 497 // Action 498 String actualHash = computeFileHash(sMicrodroid, authfsInputDir + "/input.4m"); 499 500 // Verify 501 String expectedHash = computeFileHash(sAndroid, TEST_DIR + "/input.4m"); 502 assertEquals("Expect consistent hash through /authfs/3: ", expectedHash, actualHash); 503 } 504 505 @Test testInputDirectory_OnlyAllowlistedFilesExist()506 public void testInputDirectory_OnlyAllowlistedFilesExist() throws Exception { 507 // Setup 508 String authfsInputDir = MOUNT_DIR + "/3"; 509 runFdServerOnAndroid("--open-dir 3:" + TEST_DIR, "--ro-dirs 3"); 510 runAuthFsOnMicrodroid("--remote-ro-dir 3:" + INPUT_MANIFEST_PATH + ":"); 511 512 // Verify 513 sMicrodroid.run("test -f " + authfsInputDir + "/input.4k"); 514 assertThat(sMicrodroid.runForResult("test -f " + authfsInputDir + "/input.4k.fsv_meta")) 515 .isFailed(); 516 } 517 518 @Test testReadOutputDirectory()519 public void testReadOutputDirectory() throws Exception { 520 // Setup 521 runFdServerOnAndroid("--open-dir 3:" + TEST_OUTPUT_DIR, "--rw-dirs 3"); 522 runAuthFsOnMicrodroid("--remote-new-rw-dir 3"); 523 524 // Action 525 String authfsOutputDir = MOUNT_DIR + "/3"; 526 sMicrodroid.run("mkdir -p " + authfsOutputDir + "/dir/dir2/dir3"); 527 sMicrodroid.run("touch " + authfsOutputDir + "/dir/dir2/dir3/file1"); 528 sMicrodroid.run("touch " + authfsOutputDir + "/dir/dir2/dir3/file2"); 529 sMicrodroid.run("touch " + authfsOutputDir + "/dir/dir2/dir3/file3"); 530 sMicrodroid.run("touch " + authfsOutputDir + "/file"); 531 532 // Verify 533 String[] actual = sMicrodroid.run("cd " + authfsOutputDir + "; find |sort").split("\n"); 534 String[] expected = new String[] { 535 ".", 536 "./dir", 537 "./dir/dir2", 538 "./dir/dir2/dir3", 539 "./dir/dir2/dir3/file1", 540 "./dir/dir2/dir3/file2", 541 "./dir/dir2/dir3/file3", 542 "./file"}; 543 assertEquals(expected, actual); 544 545 // Add more entries. 546 sMicrodroid.run("mkdir -p " + authfsOutputDir + "/dir2"); 547 sMicrodroid.run("touch " + authfsOutputDir + "/file2"); 548 // Check new entries. Also check that the types are correct. 549 actual = sMicrodroid.run( 550 "cd " + authfsOutputDir + "; find -maxdepth 1 -type f |sort").split("\n"); 551 expected = new String[] {"./file", "./file2"}; 552 assertEquals(expected, actual); 553 actual = sMicrodroid.run( 554 "cd " + authfsOutputDir + "; find -maxdepth 1 -type d |sort").split("\n"); 555 expected = new String[] {".", "./dir", "./dir2"}; 556 assertEquals(expected, actual); 557 } 558 559 @Test testChmod_File()560 public void testChmod_File() throws Exception { 561 // Setup 562 runFdServerOnAndroid("--open-rw 3:" + TEST_OUTPUT_DIR + "/file", "--rw-fds 3"); 563 runAuthFsOnMicrodroid("--remote-new-rw-file 3"); 564 565 // Action & Verify 566 // Change mode 567 sMicrodroid.run("chmod 321 " + MOUNT_DIR + "/3"); 568 expectFileMode("--wx-w---x", MOUNT_DIR + "/3", TEST_OUTPUT_DIR + "/file"); 569 // Can't set the disallowed bits 570 assertThat(sMicrodroid.runForResult("chmod +s " + MOUNT_DIR + "/3")).isFailed(); 571 assertThat(sMicrodroid.runForResult("chmod +t " + MOUNT_DIR + "/3")).isFailed(); 572 } 573 574 @Test testChmod_Dir()575 public void testChmod_Dir() throws Exception { 576 // Setup 577 runFdServerOnAndroid("--open-dir 3:" + TEST_OUTPUT_DIR, "--rw-dirs 3"); 578 runAuthFsOnMicrodroid("--remote-new-rw-dir 3"); 579 580 // Action & Verify 581 String authfsOutputDir = MOUNT_DIR + "/3"; 582 // Create with umask 583 sMicrodroid.run("umask 000; mkdir " + authfsOutputDir + "/dir"); 584 sMicrodroid.run("umask 022; mkdir " + authfsOutputDir + "/dir/dir2"); 585 expectFileMode("drwxrwxrwx", authfsOutputDir + "/dir", TEST_OUTPUT_DIR + "/dir"); 586 expectFileMode("drwxr-xr-x", authfsOutputDir + "/dir/dir2", TEST_OUTPUT_DIR + "/dir/dir2"); 587 // Change mode 588 sMicrodroid.run("chmod -w " + authfsOutputDir + "/dir/dir2"); 589 expectFileMode("dr-xr-xr-x", authfsOutputDir + "/dir/dir2", TEST_OUTPUT_DIR + "/dir/dir2"); 590 sMicrodroid.run("chmod 321 " + authfsOutputDir + "/dir"); 591 expectFileMode("d-wx-w---x", authfsOutputDir + "/dir", TEST_OUTPUT_DIR + "/dir"); 592 // Can't set the disallowed bits 593 assertThat(sMicrodroid.runForResult("chmod +s " + authfsOutputDir + "/dir/dir2")) 594 .isFailed(); 595 assertThat(sMicrodroid.runForResult("chmod +t " + authfsOutputDir + "/dir")).isFailed(); 596 } 597 598 @Test testChmod_FileInOutputDirectory()599 public void testChmod_FileInOutputDirectory() throws Exception { 600 // Setup 601 runFdServerOnAndroid("--open-dir 3:" + TEST_OUTPUT_DIR, "--rw-dirs 3"); 602 runAuthFsOnMicrodroid("--remote-new-rw-dir 3"); 603 604 // Action & Verify 605 String authfsOutputDir = MOUNT_DIR + "/3"; 606 // Create with umask 607 sMicrodroid.run("umask 000; echo -n foo > " + authfsOutputDir + "/file"); 608 sMicrodroid.run("umask 022; echo -n foo > " + authfsOutputDir + "/file2"); 609 expectFileMode("-rw-rw-rw-", authfsOutputDir + "/file", TEST_OUTPUT_DIR + "/file"); 610 expectFileMode("-rw-r--r--", authfsOutputDir + "/file2", TEST_OUTPUT_DIR + "/file2"); 611 // Change mode 612 sMicrodroid.run("chmod -w " + authfsOutputDir + "/file"); 613 expectFileMode("-r--r--r--", authfsOutputDir + "/file", TEST_OUTPUT_DIR + "/file"); 614 sMicrodroid.run("chmod 321 " + authfsOutputDir + "/file2"); 615 expectFileMode("--wx-w---x", authfsOutputDir + "/file2", TEST_OUTPUT_DIR + "/file2"); 616 // Can't set the disallowed bits 617 assertThat(sMicrodroid.runForResult("chmod +s " + authfsOutputDir + "/file")).isFailed(); 618 assertThat(sMicrodroid.runForResult("chmod +t " + authfsOutputDir + "/file2")).isFailed(); 619 } 620 621 @Test testStatfs()622 public void testStatfs() throws Exception { 623 // Setup 624 runFdServerOnAndroid("--open-dir 3:" + TEST_OUTPUT_DIR, "--rw-dirs 3"); 625 runAuthFsOnMicrodroid("--remote-new-rw-dir 3"); 626 627 // Verify 628 // Magic matches. Has only 2 inodes (root and "/3"). 629 assertEquals( 630 mAuthFsTestRule.FUSE_SUPER_MAGIC_HEX + " 2", 631 sMicrodroid.run("stat -f -c '%t %c' " + MOUNT_DIR)); 632 } 633 expectBackingFileConsistency( String authFsPath, String backendPath, String expectedHash)634 private void expectBackingFileConsistency( 635 String authFsPath, String backendPath, String expectedHash) 636 throws DeviceNotAvailableException { 637 String hashOnAuthFs = computeFileHash(sMicrodroid, authFsPath); 638 assertEquals("File hash is different to expectation", expectedHash, hashOnAuthFs); 639 640 String hashOfBackingFile = computeFileHash(sAndroid, backendPath); 641 assertEquals( 642 "Inconsistent file hash on the backend storage", hashOnAuthFs, hashOfBackingFile); 643 } 644 computeFileHash(CommandRunner runner, String path)645 private static String computeFileHash(CommandRunner runner, String path) 646 throws DeviceNotAvailableException { 647 String result = runner.run("sha256sum " + path); 648 String[] tokens = result.split("\\s"); 649 if (tokens.length > 0) { 650 return tokens[0]; 651 } else { 652 CLog.e("Unrecognized output by sha256sum: " + result); 653 return ""; 654 } 655 } 656 copyFile(CommandRunner runner, String src, String dest)657 private static CommandResult copyFile(CommandRunner runner, String src, String dest) 658 throws DeviceNotAvailableException { 659 // toybox's cp(1) implementation ignores most read(2) errors, and it's unclear what the 660 // canonical behavior should be (not mentioned in manpage). For this test, use cat(1) in 661 // order to fail on I/O error. 662 return runner.runForResult("cat " + src + " > " + dest); 663 } 664 expectFileMode(String expected, String microdroidPath, String androidPath)665 private void expectFileMode(String expected, String microdroidPath, String androidPath) 666 throws DeviceNotAvailableException { 667 String actual = sMicrodroid.run("stat -c '%A' " + microdroidPath); 668 assertEquals("Inconsistent mode for " + microdroidPath, expected, actual); 669 670 actual = sAndroid.run("stat -c '%A' " + androidPath); 671 assertEquals("Inconsistent mode for " + androidPath + " (android)", expected, actual); 672 } 673 resizeFile(CommandRunner runner, String path, long size)674 private static CommandResult resizeFile(CommandRunner runner, String path, long size) 675 throws DeviceNotAvailableException { 676 return runner.runForResult("truncate -c -s " + size + " " + path); 677 } 678 getFileSizeInBytes(CommandRunner runner, String path)679 private static long getFileSizeInBytes(CommandRunner runner, String path) 680 throws DeviceNotAvailableException { 681 return Long.parseLong(runner.run("stat -c '%s' " + path)); 682 } 683 createFileWithOnes(CommandRunner runner, String filePath, long numberOfOnes)684 private static void createFileWithOnes(CommandRunner runner, String filePath, long numberOfOnes) 685 throws DeviceNotAvailableException { 686 runner.run( 687 "yes $'\\x01' | tr -d '\\n' | dd bs=1 count=" + numberOfOnes + " of=" + filePath); 688 } 689 checkReadAt(CommandRunner runner, String filePath, long offset, long size)690 private static CommandResult checkReadAt(CommandRunner runner, String filePath, long offset, 691 long size) throws DeviceNotAvailableException { 692 String cmd = "dd if=" + filePath + " of=/dev/null bs=1 count=" + size; 693 if (offset > 0) { 694 cmd += " skip=" + offset; 695 } 696 return runner.runForResult(cmd); 697 } 698 writeZerosAtFileOffset(CommandRunner runner, String filePath, long offset, long numberOfZeros, boolean writeThrough)699 private CommandResult writeZerosAtFileOffset(CommandRunner runner, String filePath, long offset, 700 long numberOfZeros, boolean writeThrough) throws DeviceNotAvailableException { 701 String cmd = "dd if=/dev/zero of=" + filePath + " bs=1 count=" + numberOfZeros 702 + " conv=notrunc"; 703 if (offset > 0) { 704 cmd += " seek=" + offset; 705 } 706 if (writeThrough) { 707 cmd += " direct"; 708 } 709 return runner.runForResult(cmd); 710 } 711 runAuthFsOnMicrodroid(String flags)712 private void runAuthFsOnMicrodroid(String flags) { 713 mAuthFsTestRule.runAuthFsOnMicrodroid(flags); 714 } 715 runFdServerOnAndroid(String helperFlags, String fdServerFlags)716 private void runFdServerOnAndroid(String helperFlags, String fdServerFlags) 717 throws DeviceNotAvailableException { 718 mAuthFsTestRule.runFdServerOnAndroid(helperFlags, fdServerFlags); 719 } 720 } 721