| /* |
| * Copyright (C) 2021 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.virt.fs; |
| |
| import static com.android.tradefed.device.TestDevice.MicrodroidBuilder; |
| import static com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assert.fail; |
| import static org.junit.Assume.assumeFalse; |
| |
| import android.platform.test.annotations.RootPermissionTest; |
| import android.virt.test.CommandRunner; |
| import android.virt.test.VirtualizationTestCaseBase; |
| |
| import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper; |
| import com.android.compatibility.common.util.PollingCheck; |
| import com.android.tradefed.build.IBuildInfo; |
| import com.android.tradefed.device.DeviceNotAvailableException; |
| import com.android.tradefed.device.ITestDevice; |
| import com.android.tradefed.device.TestDevice; |
| import com.android.tradefed.invoker.TestInformation; |
| import com.android.tradefed.log.LogUtil.CLog; |
| import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; |
| import com.android.tradefed.testtype.junit4.AfterClassWithInfo; |
| import com.android.tradefed.testtype.junit4.BeforeClassWithInfo; |
| import com.android.tradefed.util.CommandResult; |
| import com.android.tradefed.util.CommandStatus; |
| |
| import org.junit.After; |
| import org.junit.AssumptionViolatedException; |
| import org.junit.Before; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.rules.TestName; |
| import org.junit.runner.RunWith; |
| |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.util.concurrent.ExecutorService; |
| import java.util.concurrent.Executors; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| |
| @RootPermissionTest |
| @RunWith(DeviceJUnit4ClassRunner.class) |
| public final class AuthFsHostTest extends VirtualizationTestCaseBase { |
| |
| /** Test directory on Android where data are located */ |
| private static final String TEST_DIR = "/data/local/tmp/authfs"; |
| |
| /** Output directory where the test can generate output on Android */ |
| private static final String TEST_OUTPUT_DIR = "/data/local/tmp/authfs/output_dir"; |
| |
| /** File name of the test APK */ |
| private static final String TEST_APK_NAME = "MicrodroidTestApp.apk"; |
| |
| /** VM config entry path in the test APK */ |
| private static final String VM_CONFIG_PATH_IN_APK = "assets/vm_config_extra_apk.json"; |
| |
| /** Path to open_then_run on Android */ |
| private static final String OPEN_THEN_RUN_BIN = "/data/local/tmp/open_then_run"; |
| |
| /** Mount point of authfs on Microdroid during the test */ |
| private static final String MOUNT_DIR = "/data/local/tmp"; |
| |
| /** Path to fd_server on Android */ |
| private static final String FD_SERVER_BIN = "/apex/com.android.virt/bin/fd_server"; |
| |
| /** Path to authfs on Microdroid */ |
| private static final String AUTHFS_BIN = "/system/bin/authfs"; |
| |
| /** Idsig paths to be created for each APK in the "extra_apks" of vm_config_extra_apk.json. */ |
| private static final String EXTRA_IDSIG_PATH = TEST_DIR + "BuildManifest.apk.idsig"; |
| |
| /** Build manifest path in the VM. 0 is the index of extra_apks in vm_config_extra_apk.json. */ |
| private static final String BUILD_MANIFEST_PATH = "/mnt/extra-apk/0/assets/build_manifest.pb"; |
| |
| /** Plenty of time for authfs to get ready */ |
| private static final int AUTHFS_INIT_TIMEOUT_MS = 3000; |
| |
| /** FUSE's magic from statfs(2) */ |
| private static final String FUSE_SUPER_MAGIC_HEX = "65735546"; |
| |
| // fs-verity digest (sha256) of testdata/input.{4k, 4k1, 4m} |
| private static final String DIGEST_4K = |
| "sha256-9828cd65f4744d6adda216d3a63d8205375be485bfa261b3b8153d3358f5a576"; |
| private static final String DIGEST_4K1 = |
| "sha256-3c70dcd4685ed256ebf1ef116c12e472f35b5017eaca422c0483dadd7d0b5a9f"; |
| private static final String DIGEST_4M = |
| "sha256-f18a268d565348fb4bbf11f10480b198f98f2922eb711de149857b3cecf98a8d"; |
| |
| private static final int VMADDR_CID_HOST = 2; |
| |
| private static CommandRunner sAndroid; |
| private static CommandRunner sMicrodroid; |
| private static boolean sAssumptionFailed; |
| |
| private ExecutorService mThreadPool = Executors.newCachedThreadPool(); |
| |
| @Rule public TestLogData mTestLogs = new TestLogData(); |
| @Rule public TestName mTestName = new TestName(); |
| |
| @BeforeClassWithInfo |
| public static void beforeClassWithDevice(TestInformation testInfo) throws Exception { |
| assertNotNull(testInfo.getDevice()); |
| ITestDevice androidDevice = testInfo.getDevice(); |
| sAndroid = new CommandRunner(androidDevice); |
| |
| if (isCuttlefish(androidDevice)) { |
| sAssumptionFailed = true; |
| return; |
| } |
| |
| try { |
| testIfDeviceIsCapable(androidDevice); |
| } catch (AssumptionViolatedException e) { |
| // NB: The assumption exception is NOT handled by the test infra when it is thrown from |
| // a class method (see b/37502066). This has not only caused the loss of log, but also |
| // prevented the test cases to be reported at all and thus confused the test infra. |
| // |
| // Since we want to avoid the big overhead to start the VM repeatedly on CF, let's catch |
| // AssumptionViolatedException and emulate it artifitially. |
| CLog.e("Assumption failed: " + e); |
| sAssumptionFailed = true; |
| return; |
| } |
| |
| // For each test case, boot and adb connect to a new Microdroid |
| CLog.i("Starting the shared VM"); |
| ITestDevice microdroidDevice = |
| MicrodroidBuilder |
| .fromFile(findTestApk(testInfo.getBuildInfo()), VM_CONFIG_PATH_IN_APK) |
| .debugLevel("full") |
| .addExtraIdsigPath(EXTRA_IDSIG_PATH) |
| .build((TestDevice) androidDevice); |
| |
| // From this point on, we need to tear down the Microdroid instance |
| sMicrodroid = new CommandRunner(microdroidDevice); |
| |
| // Root because authfs (started from shell in this test) currently require root to open |
| // /dev/fuse and mount the FUSE. |
| assertThat(microdroidDevice.enableAdbRoot()).isTrue(); |
| } |
| |
| @AfterClassWithInfo |
| public static void afterClassWithDevice(TestInformation testInfo) |
| throws DeviceNotAvailableException { |
| assertNotNull(sAndroid); |
| |
| if (sMicrodroid != null) { |
| CLog.i("Shutting down shared VM"); |
| ((TestDevice) testInfo.getDevice()).shutdownMicrodroid(sMicrodroid.getDevice()); |
| sMicrodroid = null; |
| } |
| |
| sAndroid = null; |
| } |
| |
| @Before |
| public void setUp() throws Exception { |
| assumeFalse(sAssumptionFailed); |
| sAndroid.run("mkdir " + TEST_OUTPUT_DIR); |
| } |
| |
| @After |
| public void tearDown() throws Exception { |
| if (sMicrodroid != null) { |
| sMicrodroid.tryRun("killall authfs"); |
| sMicrodroid.tryRun("umount " + MOUNT_DIR); |
| } |
| |
| assertNotNull(sAndroid); |
| sAndroid.tryRun("killall fd_server"); |
| |
| // Even though we only run one VM for the whole class, and could have collect the VM log |
| // after all tests are done, TestLogData doesn't seem to work at class level. Hence, |
| // collect recent logs manually for each test method. |
| String vmRecentLog = TEST_OUTPUT_DIR + "/vm_recent.log"; |
| sAndroid.tryRun("tail -n 50 " + LOG_PATH + " > " + vmRecentLog); |
| archiveLogThenDelete(mTestLogs, getDevice(), vmRecentLog, |
| "vm_recent.log-" + mTestName.getMethodName()); |
| |
| sAndroid.run("rm -rf " + TEST_OUTPUT_DIR); |
| } |
| |
| @Test |
| public void testReadWithFsverityVerification_RemoteFile() throws Exception { |
| // Setup |
| runFdServerOnAndroid( |
| "--open-ro 3:input.4m --open-ro 4:input.4m.fsv_meta --open-ro 6:input.4m", |
| "--ro-fds 3:4 --ro-fds 6"); |
| |
| runAuthFsOnMicrodroid( |
| "--remote-ro-file-unverified 6 --remote-ro-file 3:" + DIGEST_4M + " --cid " |
| + VMADDR_CID_HOST); |
| |
| // Action |
| String actualHashUnverified4m = computeFileHashOnMicrodroid(MOUNT_DIR + "/6"); |
| String actualHash4m = computeFileHashOnMicrodroid(MOUNT_DIR + "/3"); |
| |
| // Verify |
| String expectedHash4m = computeFileHashOnAndroid(TEST_DIR + "/input.4m"); |
| |
| assertEquals("Inconsistent hash from /authfs/6: ", expectedHash4m, actualHashUnverified4m); |
| assertEquals("Inconsistent hash from /authfs/3: ", expectedHash4m, actualHash4m); |
| } |
| |
| // Separate the test from the above simply because exec in shell does not allow open too many |
| // files. |
| @Test |
| public void testReadWithFsverityVerification_RemoteSmallerFile() throws Exception { |
| // Setup |
| runFdServerOnAndroid( |
| "--open-ro 3:input.4k --open-ro 4:input.4k.fsv_meta --open-ro" |
| + " 6:input.4k1 --open-ro 7:input.4k1.fsv_meta", |
| "--ro-fds 3:4 --ro-fds 6:7"); |
| runAuthFsOnMicrodroid( |
| "--remote-ro-file 3:" + DIGEST_4K + " --remote-ro-file 6:" + DIGEST_4K1 + " --cid " |
| + VMADDR_CID_HOST); |
| |
| // Action |
| String actualHash4k = computeFileHashOnMicrodroid(MOUNT_DIR + "/3"); |
| String actualHash4k1 = computeFileHashOnMicrodroid(MOUNT_DIR + "/6"); |
| |
| // Verify |
| String expectedHash4k = computeFileHashOnAndroid(TEST_DIR + "/input.4k"); |
| String expectedHash4k1 = computeFileHashOnAndroid(TEST_DIR + "/input.4k1"); |
| |
| assertEquals("Inconsistent hash from /authfs/3: ", expectedHash4k, actualHash4k); |
| assertEquals("Inconsistent hash from /authfs/6: ", expectedHash4k1, actualHash4k1); |
| } |
| |
| @Test |
| public void testReadWithFsverityVerification_TamperedMerkleTree() throws Exception { |
| // Setup |
| runFdServerOnAndroid( |
| "--open-ro 3:input.4m --open-ro 4:input.4m.fsv_meta.bad_merkle", |
| "--ro-fds 3:4"); |
| runAuthFsOnMicrodroid("--remote-ro-file 3:" + DIGEST_4M + " --cid " + VMADDR_CID_HOST); |
| |
| // Verify |
| assertFalse(copyFileOnMicrodroid(MOUNT_DIR + "/3", "/dev/null")); |
| } |
| |
| @Test |
| public void testWriteThroughCorrectly() throws Exception { |
| // Setup |
| runFdServerOnAndroid("--open-rw 3:" + TEST_OUTPUT_DIR + "/out.file", "--rw-fds 3"); |
| runAuthFsOnMicrodroid("--remote-new-rw-file 3 --cid " + VMADDR_CID_HOST); |
| |
| // Action |
| String srcPath = "/system/bin/linker64"; |
| String destPath = MOUNT_DIR + "/3"; |
| String backendPath = TEST_OUTPUT_DIR + "/out.file"; |
| assertTrue(copyFileOnMicrodroid(srcPath, destPath)); |
| |
| // Verify |
| String expectedHash = computeFileHashOnMicrodroid(srcPath); |
| expectBackingFileConsistency(destPath, backendPath, expectedHash); |
| } |
| |
| @Test |
| public void testWriteFailedIfDetectsTampering() throws Exception { |
| // Setup |
| runFdServerOnAndroid("--open-rw 3:" + TEST_OUTPUT_DIR + "/out.file", "--rw-fds 3"); |
| runAuthFsOnMicrodroid("--remote-new-rw-file 3 --cid " + VMADDR_CID_HOST); |
| |
| String srcPath = "/system/bin/linker64"; |
| String destPath = MOUNT_DIR + "/3"; |
| String backendPath = TEST_OUTPUT_DIR + "/out.file"; |
| assertTrue(copyFileOnMicrodroid(srcPath, destPath)); |
| |
| // Action |
| // Tampering with the first 2 4K-blocks of the backing file. |
| zeroizeFileOnAndroid(backendPath, /* size */ 8192, /* offset */ 0); |
| |
| // Verify |
| // Write to a block partially requires a read back to calculate the new hash. It should fail |
| // when the content is inconsistent to the known hash. Use direct I/O to avoid simply |
| // writing to the filesystem cache. |
| assertFalse( |
| writeZerosAtFileOffsetOnMicrodroid( |
| destPath, /* offset */ 0, /* number */ 1024, /* writeThrough */ true)); |
| |
| // A full 4K write does not require to read back, so write can succeed even if the backing |
| // block has already been tampered. |
| assertTrue( |
| writeZerosAtFileOffsetOnMicrodroid( |
| destPath, /* offset */ 4096, /* number */ 4096, /* writeThrough */ false)); |
| |
| // Otherwise, a partial write with correct backing file should still succeed. |
| assertTrue( |
| writeZerosAtFileOffsetOnMicrodroid( |
| destPath, /* offset */ 8192, /* number */ 1024, /* writeThrough */ false)); |
| } |
| |
| @Test |
| public void testReadFailedIfDetectsTampering() throws Exception { |
| // Setup |
| runFdServerOnAndroid("--open-rw 3:" + TEST_OUTPUT_DIR + "/out.file", "--rw-fds 3"); |
| runAuthFsOnMicrodroid("--remote-new-rw-file 3 --cid " + VMADDR_CID_HOST); |
| |
| String srcPath = "/system/bin/linker64"; |
| String destPath = MOUNT_DIR + "/3"; |
| String backendPath = TEST_OUTPUT_DIR + "/out.file"; |
| assertTrue(copyFileOnMicrodroid(srcPath, destPath)); |
| |
| // Action |
| // Tampering with the first 4K-block of the backing file. |
| zeroizeFileOnAndroid(backendPath, /* size */ 4096, /* offset */ 0); |
| |
| // Verify |
| // Force dropping the page cache, so that the next read can be validated. |
| sMicrodroid.run("echo 1 > /proc/sys/vm/drop_caches"); |
| // A read will fail if the backing data has been tampered. |
| assertFalse(checkReadAtFileOffsetOnMicrodroid( |
| destPath, /* offset */ 0, /* number */ 4096)); |
| assertTrue(checkReadAtFileOffsetOnMicrodroid( |
| destPath, /* offset */ 4096, /* number */ 4096)); |
| } |
| |
| @Test |
| public void testResizeFailedIfDetectsTampering() throws Exception { |
| // Setup |
| runFdServerOnAndroid("--open-rw 3:" + TEST_OUTPUT_DIR + "/out.file", "--rw-fds 3"); |
| runAuthFsOnMicrodroid("--remote-new-rw-file 3 --cid " + VMADDR_CID_HOST); |
| |
| String outputPath = MOUNT_DIR + "/3"; |
| String backendPath = TEST_OUTPUT_DIR + "/out.file"; |
| createFileWithOnesOnMicrodroid(outputPath, 8192); |
| |
| // Action |
| // Tampering with the last 4K-block of the backing file. |
| zeroizeFileOnAndroid(backendPath, /* size */ 1, /* offset */ 4096); |
| |
| // Verify |
| // A resize (to a non-multiple of 4K) will fail if the last backing chunk has been |
| // tampered. The original data is necessary (and has to be verified) to calculate the new |
| // hash with shorter data. |
| assertFalse(resizeFileOnMicrodroid(outputPath, 8000)); |
| } |
| |
| @Test |
| public void testFileResize() throws Exception { |
| // Setup |
| runFdServerOnAndroid("--open-rw 3:" + TEST_OUTPUT_DIR + "/out.file", "--rw-fds 3"); |
| runAuthFsOnMicrodroid("--remote-new-rw-file 3 --cid " + VMADDR_CID_HOST); |
| String outputPath = MOUNT_DIR + "/3"; |
| String backendPath = TEST_OUTPUT_DIR + "/out.file"; |
| |
| // Action & Verify |
| createFileWithOnesOnMicrodroid(outputPath, 10000); |
| assertEquals(getFileSizeInBytesOnMicrodroid(outputPath), 10000); |
| expectBackingFileConsistency( |
| outputPath, |
| backendPath, |
| "684ad25fdc2bbb80cbc910dd1bde6d5499ccf860ca6ee44704b77ec445271353"); |
| |
| assertTrue(resizeFileOnMicrodroid(outputPath, 15000)); |
| assertEquals(getFileSizeInBytesOnMicrodroid(outputPath), 15000); |
| expectBackingFileConsistency( |
| outputPath, |
| backendPath, |
| "567c89f62586e0d33369157afdfe99a2fa36cdffb01e91dcdc0b7355262d610d"); |
| |
| assertTrue(resizeFileOnMicrodroid(outputPath, 5000)); |
| assertEquals(getFileSizeInBytesOnMicrodroid(outputPath), 5000); |
| expectBackingFileConsistency( |
| outputPath, |
| backendPath, |
| "e53130831c13dabff71d5d1797e3aaa467b4b7d32b3b8782c4ff03d76976f2aa"); |
| } |
| |
| @Test |
| public void testOutputDirectory_WriteNewFiles() throws Exception { |
| // Setup |
| String androidOutputDir = TEST_OUTPUT_DIR + "/dir"; |
| String authfsOutputDir = MOUNT_DIR + "/3"; |
| sAndroid.run("mkdir " + androidOutputDir); |
| runFdServerOnAndroid("--open-dir 3:" + androidOutputDir, "--rw-dirs 3"); |
| runAuthFsOnMicrodroid("--remote-new-rw-dir 3 --cid " + VMADDR_CID_HOST); |
| |
| // Action & Verify |
| // Can create a new file to write. |
| String expectedAndroidPath = androidOutputDir + "/file"; |
| String authfsPath = authfsOutputDir + "/file"; |
| createFileWithOnesOnMicrodroid(authfsPath, 10000); |
| assertEquals(getFileSizeInBytesOnMicrodroid(authfsPath), 10000); |
| expectBackingFileConsistency( |
| authfsPath, |
| expectedAndroidPath, |
| "684ad25fdc2bbb80cbc910dd1bde6d5499ccf860ca6ee44704b77ec445271353"); |
| |
| // Regular file operations work, e.g. resize. |
| assertTrue(resizeFileOnMicrodroid(authfsPath, 15000)); |
| assertEquals(getFileSizeInBytesOnMicrodroid(authfsPath), 15000); |
| expectBackingFileConsistency( |
| authfsPath, |
| expectedAndroidPath, |
| "567c89f62586e0d33369157afdfe99a2fa36cdffb01e91dcdc0b7355262d610d"); |
| } |
| |
| @Test |
| public void testOutputDirectory_MkdirAndWriteFile() throws Exception { |
| // Setup |
| String androidOutputDir = TEST_OUTPUT_DIR + "/dir"; |
| String authfsOutputDir = MOUNT_DIR + "/3"; |
| sAndroid.run("mkdir " + androidOutputDir); |
| runFdServerOnAndroid("--open-dir 3:" + androidOutputDir, "--rw-dirs 3"); |
| runAuthFsOnMicrodroid("--remote-new-rw-dir 3 --cid " + VMADDR_CID_HOST); |
| |
| // Action |
| // Can create nested directories and can create a file in one. |
| sMicrodroid.run("mkdir " + authfsOutputDir + "/new_dir"); |
| sMicrodroid.run("mkdir -p " + authfsOutputDir + "/we/need/to/go/deeper"); |
| createFileWithOnesOnMicrodroid(authfsOutputDir + "/new_dir/file1", 10000); |
| createFileWithOnesOnMicrodroid(authfsOutputDir + "/we/need/file2", 10000); |
| |
| // Verify |
| // Directories show up in Android. |
| sAndroid.run("test -d " + androidOutputDir + "/new_dir"); |
| sAndroid.run("test -d " + androidOutputDir + "/we/need/to/go/deeper"); |
| // Files exist in Android. Hashes on Microdroid and Android are consistent. |
| assertEquals(getFileSizeInBytesOnMicrodroid(authfsOutputDir + "/new_dir/file1"), 10000); |
| expectBackingFileConsistency( |
| authfsOutputDir + "/new_dir/file1", |
| androidOutputDir + "/new_dir/file1", |
| "684ad25fdc2bbb80cbc910dd1bde6d5499ccf860ca6ee44704b77ec445271353"); |
| // Same to file in a nested directory. |
| assertEquals(getFileSizeInBytesOnMicrodroid(authfsOutputDir + "/we/need/file2"), 10000); |
| expectBackingFileConsistency( |
| authfsOutputDir + "/we/need/file2", |
| androidOutputDir + "/we/need/file2", |
| "684ad25fdc2bbb80cbc910dd1bde6d5499ccf860ca6ee44704b77ec445271353"); |
| } |
| |
| @Test |
| public void testOutputDirectory_CreateAndTruncateExistingFile() throws Exception { |
| // Setup |
| String androidOutputDir = TEST_OUTPUT_DIR + "/dir"; |
| String authfsOutputDir = MOUNT_DIR + "/3"; |
| sAndroid.run("mkdir " + androidOutputDir); |
| runFdServerOnAndroid("--open-dir 3:" + androidOutputDir, "--rw-dirs 3"); |
| runAuthFsOnMicrodroid("--remote-new-rw-dir 3 --cid " + VMADDR_CID_HOST); |
| |
| // Action & Verify |
| sMicrodroid.run("echo -n foo > " + authfsOutputDir + "/file"); |
| assertEquals(getFileSizeInBytesOnMicrodroid(authfsOutputDir + "/file"), 3); |
| // Can override a file and write normally. |
| createFileWithOnesOnMicrodroid(authfsOutputDir + "/file", 10000); |
| assertEquals(getFileSizeInBytesOnMicrodroid(authfsOutputDir + "/file"), 10000); |
| expectBackingFileConsistency( |
| authfsOutputDir + "/file", |
| androidOutputDir + "/file", |
| "684ad25fdc2bbb80cbc910dd1bde6d5499ccf860ca6ee44704b77ec445271353"); |
| } |
| |
| @Test |
| public void testOutputDirectory_CanDeleteFile() throws Exception { |
| // Setup |
| String androidOutputDir = TEST_OUTPUT_DIR + "/dir"; |
| String authfsOutputDir = MOUNT_DIR + "/3"; |
| sAndroid.run("mkdir " + androidOutputDir); |
| runFdServerOnAndroid("--open-dir 3:" + androidOutputDir, "--rw-dirs 3"); |
| runAuthFsOnMicrodroid("--remote-new-rw-dir 3 --cid " + VMADDR_CID_HOST); |
| |
| sMicrodroid.run("echo -n foo > " + authfsOutputDir + "/file"); |
| sMicrodroid.run("test -f " + authfsOutputDir + "/file"); |
| sAndroid.run("test -f " + androidOutputDir + "/file"); |
| |
| // Action & Verify |
| sMicrodroid.run("rm " + authfsOutputDir + "/file"); |
| sMicrodroid.run("test ! -f " + authfsOutputDir + "/file"); |
| sAndroid.run("test ! -f " + androidOutputDir + "/file"); |
| } |
| |
| @Test |
| public void testOutputDirectory_CanDeleteDirectoryOnlyIfEmpty() throws Exception { |
| // Setup |
| String androidOutputDir = TEST_OUTPUT_DIR + "/dir"; |
| String authfsOutputDir = MOUNT_DIR + "/3"; |
| sAndroid.run("mkdir " + androidOutputDir); |
| runFdServerOnAndroid("--open-dir 3:" + androidOutputDir, "--rw-dirs 3"); |
| runAuthFsOnMicrodroid("--remote-new-rw-dir 3 --cid " + VMADDR_CID_HOST); |
| |
| sMicrodroid.run("mkdir -p " + authfsOutputDir + "/dir/dir2"); |
| sMicrodroid.run("echo -n foo > " + authfsOutputDir + "/dir/file"); |
| sAndroid.run("test -d " + androidOutputDir + "/dir/dir2"); |
| |
| // Action & Verify |
| sMicrodroid.run("rmdir " + authfsOutputDir + "/dir/dir2"); |
| sMicrodroid.run("test ! -d " + authfsOutputDir + "/dir/dir2"); |
| sAndroid.run("test ! -d " + androidOutputDir + "/dir/dir2"); |
| // Can only delete a directory if empty |
| assertFailedOnMicrodroid("rmdir " + authfsOutputDir + "/dir"); |
| sMicrodroid.run("test -d " + authfsOutputDir + "/dir"); // still there |
| sMicrodroid.run("rm " + authfsOutputDir + "/dir/file"); |
| sMicrodroid.run("rmdir " + authfsOutputDir + "/dir"); |
| sMicrodroid.run("test ! -d " + authfsOutputDir + "/dir"); |
| sAndroid.run("test ! -d " + androidOutputDir + "/dir"); |
| } |
| |
| @Test |
| public void testOutputDirectory_CannotRecreateDirectoryIfNameExists() throws Exception { |
| // Setup |
| String androidOutputDir = TEST_OUTPUT_DIR + "/dir"; |
| String authfsOutputDir = MOUNT_DIR + "/3"; |
| sAndroid.run("mkdir " + androidOutputDir); |
| runFdServerOnAndroid("--open-dir 3:" + androidOutputDir, "--rw-dirs 3"); |
| runAuthFsOnMicrodroid("--remote-new-rw-dir 3 --cid " + VMADDR_CID_HOST); |
| |
| sMicrodroid.run("touch " + authfsOutputDir + "/some_file"); |
| sMicrodroid.run("mkdir " + authfsOutputDir + "/some_dir"); |
| sMicrodroid.run("touch " + authfsOutputDir + "/some_dir/file"); |
| sMicrodroid.run("mkdir " + authfsOutputDir + "/some_dir/dir"); |
| |
| // Action & Verify |
| // Cannot create directory if an entry with the same name already exists. |
| assertFailedOnMicrodroid("mkdir " + authfsOutputDir + "/some_file"); |
| assertFailedOnMicrodroid("mkdir " + authfsOutputDir + "/some_dir"); |
| assertFailedOnMicrodroid("mkdir " + authfsOutputDir + "/some_dir/file"); |
| assertFailedOnMicrodroid("mkdir " + authfsOutputDir + "/some_dir/dir"); |
| } |
| |
| @Test |
| public void testOutputDirectory_WriteToFdOfDeletedFile() throws Exception { |
| // Setup |
| String authfsOutputDir = MOUNT_DIR + "/3"; |
| String androidOutputDir = TEST_OUTPUT_DIR + "/dir"; |
| sAndroid.run("mkdir " + androidOutputDir); |
| runFdServerOnAndroid("--open-dir 3:" + androidOutputDir, "--rw-dirs 3"); |
| runAuthFsOnMicrodroid("--remote-new-rw-dir 3 --cid " + VMADDR_CID_HOST); |
| |
| // Create a file with some data. Test the existence. |
| String outputPath = authfsOutputDir + "/out"; |
| String androidOutputPath = androidOutputDir + "/out"; |
| sMicrodroid.run("echo -n 123 > " + outputPath); |
| sMicrodroid.run("test -f " + outputPath); |
| sAndroid.run("test -f " + androidOutputPath); |
| |
| // Action |
| String output = sMicrodroid.run( |
| // Open the file for append and read |
| "exec 4>>" + outputPath + " 5<" + outputPath + "; " |
| // Delete the file from the directory |
| + "rm " + outputPath + "; " |
| // Append more data to the file descriptor |
| + "echo -n 456 >&4; " |
| // Print the whole file from the file descriptor |
| + "cat <&5"); |
| |
| // Verify |
| // Output contains all written data, while the files are deleted. |
| assertEquals("123456", output); |
| sMicrodroid.run("test ! -f " + outputPath); |
| sAndroid.run("test ! -f " + androidOutputDir + "/out"); |
| } |
| |
| @Test |
| public void testInputDirectory_CanReadFile() throws Exception { |
| // Setup |
| String authfsInputDir = MOUNT_DIR + "/3"; |
| runFdServerOnAndroid("--open-dir 3:/system", "--ro-dirs 3"); |
| runAuthFsOnMicrodroid("--remote-ro-dir 3:" + BUILD_MANIFEST_PATH + ":system/ --cid " |
| + VMADDR_CID_HOST); |
| |
| // Action |
| String actualHash = |
| computeFileHashOnMicrodroid(authfsInputDir + "/system/framework/framework.jar"); |
| |
| // Verify |
| String expectedHash = computeFileHashOnAndroid("/system/framework/framework.jar"); |
| assertEquals("Expect consistent hash through /authfs/3: ", expectedHash, actualHash); |
| } |
| |
| @Test |
| public void testInputDirectory_OnlyAllowlistedFilesExist() throws Exception { |
| // Setup |
| String authfsInputDir = MOUNT_DIR + "/3"; |
| runFdServerOnAndroid("--open-dir 3:/system", "--ro-dirs 3"); |
| runAuthFsOnMicrodroid("--remote-ro-dir 3:" + BUILD_MANIFEST_PATH + ":system/ --cid " |
| + VMADDR_CID_HOST); |
| |
| // Verify |
| sMicrodroid.run("test -f " + authfsInputDir + "/system/framework/services.jar"); |
| assertFailedOnMicrodroid("test -f " + authfsInputDir + "/system/bin/sh"); |
| } |
| |
| @Test |
| public void testReadOutputDirectory() throws Exception { |
| // Setup |
| runFdServerOnAndroid("--open-dir 3:" + TEST_OUTPUT_DIR, "--rw-dirs 3"); |
| runAuthFsOnMicrodroid("--remote-new-rw-dir 3 --cid " + VMADDR_CID_HOST); |
| |
| // Action |
| String authfsOutputDir = MOUNT_DIR + "/3"; |
| sMicrodroid.run("mkdir -p " + authfsOutputDir + "/dir/dir2/dir3"); |
| sMicrodroid.run("touch " + authfsOutputDir + "/dir/dir2/dir3/file1"); |
| sMicrodroid.run("touch " + authfsOutputDir + "/dir/dir2/dir3/file2"); |
| sMicrodroid.run("touch " + authfsOutputDir + "/dir/dir2/dir3/file3"); |
| sMicrodroid.run("touch " + authfsOutputDir + "/file"); |
| |
| // Verify |
| String[] actual = sMicrodroid.run("cd " + authfsOutputDir + "; find |sort").split("\n"); |
| String[] expected = new String[] { |
| ".", |
| "./dir", |
| "./dir/dir2", |
| "./dir/dir2/dir3", |
| "./dir/dir2/dir3/file1", |
| "./dir/dir2/dir3/file2", |
| "./dir/dir2/dir3/file3", |
| "./file"}; |
| assertEquals(expected, actual); |
| |
| // Add more entries. |
| sMicrodroid.run("mkdir -p " + authfsOutputDir + "/dir2"); |
| sMicrodroid.run("touch " + authfsOutputDir + "/file2"); |
| // Check new entries. Also check that the types are correct. |
| actual = sMicrodroid.run( |
| "cd " + authfsOutputDir + "; find -maxdepth 1 -type f |sort").split("\n"); |
| expected = new String[] {"./file", "./file2"}; |
| assertEquals(expected, actual); |
| actual = sMicrodroid.run( |
| "cd " + authfsOutputDir + "; find -maxdepth 1 -type d |sort").split("\n"); |
| expected = new String[] {".", "./dir", "./dir2"}; |
| assertEquals(expected, actual); |
| } |
| |
| @Test |
| public void testChmod_File() throws Exception { |
| // Setup |
| runFdServerOnAndroid("--open-rw 3:" + TEST_OUTPUT_DIR + "/file", "--rw-fds 3"); |
| runAuthFsOnMicrodroid("--remote-new-rw-file 3 --cid " + VMADDR_CID_HOST); |
| |
| // Action & Verify |
| // Change mode |
| sMicrodroid.run("chmod 321 " + MOUNT_DIR + "/3"); |
| expectFileMode("--wx-w---x", MOUNT_DIR + "/3", TEST_OUTPUT_DIR + "/file"); |
| // Can't set the disallowed bits |
| assertFailedOnMicrodroid("chmod +s " + MOUNT_DIR + "/3"); |
| assertFailedOnMicrodroid("chmod +t " + MOUNT_DIR + "/3"); |
| } |
| |
| @Test |
| public void testChmod_Dir() throws Exception { |
| // Setup |
| runFdServerOnAndroid("--open-dir 3:" + TEST_OUTPUT_DIR, "--rw-dirs 3"); |
| runAuthFsOnMicrodroid("--remote-new-rw-dir 3 --cid " + VMADDR_CID_HOST); |
| |
| // Action & Verify |
| String authfsOutputDir = MOUNT_DIR + "/3"; |
| // Create with umask |
| sMicrodroid.run("umask 000; mkdir " + authfsOutputDir + "/dir"); |
| sMicrodroid.run("umask 022; mkdir " + authfsOutputDir + "/dir/dir2"); |
| expectFileMode("drwxrwxrwx", authfsOutputDir + "/dir", TEST_OUTPUT_DIR + "/dir"); |
| expectFileMode("drwxr-xr-x", authfsOutputDir + "/dir/dir2", TEST_OUTPUT_DIR + "/dir/dir2"); |
| // Change mode |
| sMicrodroid.run("chmod -w " + authfsOutputDir + "/dir/dir2"); |
| expectFileMode("dr-xr-xr-x", authfsOutputDir + "/dir/dir2", TEST_OUTPUT_DIR + "/dir/dir2"); |
| sMicrodroid.run("chmod 321 " + authfsOutputDir + "/dir"); |
| expectFileMode("d-wx-w---x", authfsOutputDir + "/dir", TEST_OUTPUT_DIR + "/dir"); |
| // Can't set the disallowed bits |
| assertFailedOnMicrodroid("chmod +s " + authfsOutputDir + "/dir/dir2"); |
| assertFailedOnMicrodroid("chmod +t " + authfsOutputDir + "/dir"); |
| } |
| |
| @Test |
| public void testChmod_FileInOutputDirectory() throws Exception { |
| // Setup |
| runFdServerOnAndroid("--open-dir 3:" + TEST_OUTPUT_DIR, "--rw-dirs 3"); |
| runAuthFsOnMicrodroid("--remote-new-rw-dir 3 --cid " + VMADDR_CID_HOST); |
| |
| // Action & Verify |
| String authfsOutputDir = MOUNT_DIR + "/3"; |
| // Create with umask |
| sMicrodroid.run("umask 000; echo -n foo > " + authfsOutputDir + "/file"); |
| sMicrodroid.run("umask 022; echo -n foo > " + authfsOutputDir + "/file2"); |
| expectFileMode("-rw-rw-rw-", authfsOutputDir + "/file", TEST_OUTPUT_DIR + "/file"); |
| expectFileMode("-rw-r--r--", authfsOutputDir + "/file2", TEST_OUTPUT_DIR + "/file2"); |
| // Change mode |
| sMicrodroid.run("chmod -w " + authfsOutputDir + "/file"); |
| expectFileMode("-r--r--r--", authfsOutputDir + "/file", TEST_OUTPUT_DIR + "/file"); |
| sMicrodroid.run("chmod 321 " + authfsOutputDir + "/file2"); |
| expectFileMode("--wx-w---x", authfsOutputDir + "/file2", TEST_OUTPUT_DIR + "/file2"); |
| // Can't set the disallowed bits |
| assertFailedOnMicrodroid("chmod +s " + authfsOutputDir + "/file"); |
| assertFailedOnMicrodroid("chmod +t " + authfsOutputDir + "/file2"); |
| } |
| |
| @Test |
| public void testStatfs() throws Exception { |
| // Setup |
| runFdServerOnAndroid("--open-dir 3:" + TEST_OUTPUT_DIR, "--rw-dirs 3"); |
| runAuthFsOnMicrodroid("--remote-new-rw-dir 3 --cid " + VMADDR_CID_HOST); |
| |
| // Verify |
| // Magic matches. Has only 2 inodes (root and "/3"). |
| assertEquals( |
| FUSE_SUPER_MAGIC_HEX + " 2", sMicrodroid.run("stat -f -c '%t %c' " + MOUNT_DIR)); |
| } |
| |
| private static File findTestApk(IBuildInfo buildInfo) { |
| try { |
| return (new CompatibilityBuildHelper(buildInfo)).getTestFile(TEST_APK_NAME); |
| } catch (FileNotFoundException e) { |
| fail("Missing test file: " + TEST_APK_NAME); |
| return null; |
| } |
| } |
| |
| private void expectBackingFileConsistency( |
| String authFsPath, String backendPath, String expectedHash) |
| throws DeviceNotAvailableException { |
| String hashOnAuthFs = computeFileHashOnMicrodroid(authFsPath); |
| assertEquals("File hash is different to expectation", expectedHash, hashOnAuthFs); |
| |
| String hashOfBackingFile = computeFileHashOnAndroid(backendPath); |
| assertEquals( |
| "Inconsistent file hash on the backend storage", hashOnAuthFs, hashOfBackingFile); |
| } |
| |
| private String computeFileHashOnMicrodroid(String path) throws DeviceNotAvailableException { |
| String result = sMicrodroid.run("sha256sum " + path); |
| String[] tokens = result.split("\\s"); |
| if (tokens.length > 0) { |
| return tokens[0]; |
| } else { |
| CLog.e("Unrecognized output by sha256sum: " + result); |
| return ""; |
| } |
| } |
| |
| private boolean copyFileOnMicrodroid(String src, String dest) |
| throws DeviceNotAvailableException { |
| // TODO(b/182576497): cp returns error because close(2) returns ENOSYS in the current authfs |
| // implementation. We should probably fix that since programs can expect close(2) return 0. |
| String cmd = "cat " + src + " > " + dest; |
| return sMicrodroid.tryRun(cmd) != null; |
| } |
| |
| private String computeFileHashOnAndroid(String path) throws DeviceNotAvailableException { |
| String result = sAndroid.run("sha256sum " + path); |
| String[] tokens = result.split("\\s"); |
| if (tokens.length > 0) { |
| return tokens[0]; |
| } else { |
| CLog.e("Unrecognized output by sha256sum: " + result); |
| return ""; |
| } |
| } |
| |
| private void expectFileMode(String expected, String microdroidPath, String androidPath) |
| throws DeviceNotAvailableException { |
| String actual = sMicrodroid.run("stat -c '%A' " + microdroidPath); |
| assertEquals("Inconsistent mode for " + microdroidPath, expected, actual); |
| |
| actual = sAndroid.run("stat -c '%A' " + androidPath); |
| assertEquals("Inconsistent mode for " + androidPath + " (android)", expected, actual); |
| } |
| |
| private boolean resizeFileOnMicrodroid(String path, long size) |
| throws DeviceNotAvailableException { |
| CommandResult result = sMicrodroid.runForResult("truncate -c -s " + size + " " + path); |
| return result.getStatus() == CommandStatus.SUCCESS; |
| } |
| |
| private long getFileSizeInBytesOnMicrodroid(String path) throws DeviceNotAvailableException { |
| return Long.parseLong(sMicrodroid.run("stat -c '%s' " + path)); |
| } |
| |
| private void createFileWithOnesOnMicrodroid(String filePath, long numberOfOnes) |
| throws DeviceNotAvailableException { |
| sMicrodroid.run( |
| "yes $'\\x01' | tr -d '\\n' | dd bs=1 count=" + numberOfOnes + " of=" + filePath); |
| } |
| |
| private boolean checkReadAtFileOffsetOnMicrodroid(String filePath, long offset, long size) |
| throws DeviceNotAvailableException { |
| String cmd = "dd if=" + filePath + " of=/dev/null bs=1 count=" + size; |
| if (offset > 0) { |
| cmd += " skip=" + offset; |
| } |
| CommandResult result = sMicrodroid.runForResult(cmd); |
| return result.getStatus() == CommandStatus.SUCCESS; |
| } |
| |
| private boolean writeZerosAtFileOffsetOnMicrodroid( |
| String filePath, long offset, long numberOfZeros, boolean writeThrough) |
| throws DeviceNotAvailableException { |
| String cmd = "dd if=/dev/zero of=" + filePath + " bs=1 count=" + numberOfZeros |
| + " conv=notrunc"; |
| if (offset > 0) { |
| cmd += " seek=" + offset; |
| } |
| if (writeThrough) { |
| cmd += " direct"; |
| } |
| CommandResult result = sMicrodroid.runForResult(cmd); |
| return result.getStatus() == CommandStatus.SUCCESS; |
| } |
| |
| private void zeroizeFileOnAndroid(String filePath, long size, long offset) |
| throws DeviceNotAvailableException { |
| sAndroid.run("dd if=/dev/zero of=" + filePath + " bs=1 count=" + size + " conv=notrunc" |
| + " seek=" + offset); |
| } |
| |
| private void runAuthFsOnMicrodroid(String flags) { |
| String cmd = AUTHFS_BIN + " " + MOUNT_DIR + " " + flags; |
| |
| AtomicBoolean starting = new AtomicBoolean(true); |
| mThreadPool.submit( |
| () -> { |
| // authfs may fail to start if fd_server is not yet listening on the vsock |
| // ("Error: Invalid raw AIBinder"). Just restart if that happens. |
| while (starting.get()) { |
| try { |
| CLog.i("Starting authfs"); |
| CommandResult result = sMicrodroid.runForResult(cmd); |
| CLog.w("authfs has stopped: " + result); |
| } catch (DeviceNotAvailableException e) { |
| CLog.e("Error running authfs", e); |
| throw new RuntimeException(e); |
| } |
| } |
| }); |
| try { |
| PollingCheck.waitFor( |
| AUTHFS_INIT_TIMEOUT_MS, () -> isMicrodroidDirectoryOnFuse(MOUNT_DIR)); |
| } catch (Exception e) { |
| // Convert the broad Exception into an unchecked exception to avoid polluting all other |
| // methods. waitFor throws Exception because the callback, Callable#call(), has a |
| // signature to throw an Exception. |
| throw new RuntimeException(e); |
| } finally { |
| starting.set(false); |
| } |
| } |
| |
| private void runFdServerOnAndroid(String helperFlags, String fdServerFlags) |
| throws DeviceNotAvailableException { |
| String cmd = |
| "cd " |
| + TEST_DIR |
| + " && " |
| + OPEN_THEN_RUN_BIN |
| + " " |
| + helperFlags |
| + " -- " |
| + FD_SERVER_BIN |
| + " " |
| + fdServerFlags; |
| |
| mThreadPool.submit( |
| () -> { |
| try { |
| CLog.i("Starting fd_server"); |
| CommandResult result = sAndroid.runForResult(cmd); |
| CLog.w("fd_server has stopped: " + result); |
| } catch (DeviceNotAvailableException e) { |
| CLog.e("Error running fd_server", e); |
| throw new RuntimeException(e); |
| } |
| }); |
| } |
| |
| private boolean isMicrodroidDirectoryOnFuse(String path) throws DeviceNotAvailableException { |
| String fs_type = sMicrodroid.tryRun("stat -f -c '%t' " + path); |
| return FUSE_SUPER_MAGIC_HEX.equals(fs_type); |
| } |
| } |