Merge "microdroid: Increase arm64 test memory"
diff --git a/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java b/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
index b332543..749f3c1 100644
--- a/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
+++ b/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
@@ -16,21 +16,21 @@
 
 package com.android.virt.fs;
 
+import static android.virt.test.CommandResultSubject.assertThat;
+import static android.virt.test.LogArchiver.archiveLogThenDelete;
+
 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 static org.junit.Assume.assumeTrue;
 
 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;
@@ -42,14 +42,12 @@
 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.BaseHostJUnit4Test;
 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.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TestName;
@@ -63,8 +61,7 @@
 
 @RootPermissionTest
 @RunWith(DeviceJUnit4ClassRunner.class)
-@Ignore("TODO(b/229823049): Make this work")
-public final class AuthFsHostTest extends VirtualizationTestCaseBase {
+public final class AuthFsHostTest extends BaseHostJUnit4Test {
 
     /** Test directory on Android where data are located */
     private static final String TEST_DIR = "/data/local/tmp/authfs";
@@ -72,6 +69,9 @@
     /** Output directory where the test can generate output on Android */
     private static final String TEST_OUTPUT_DIR = "/data/local/tmp/authfs/output_dir";
 
+    /** VM's log file */
+    private static final String LOG_PATH = TEST_OUTPUT_DIR + "/log.txt";
+
     /** File name of the test APK */
     private static final String TEST_APK_NAME = "MicrodroidTestApp.apk";
 
@@ -124,20 +124,18 @@
     @BeforeClassWithInfo
     public static void beforeClassWithDevice(TestInformation testInfo) throws Exception {
         assertNotNull(testInfo.getDevice());
-        ITestDevice androidDevice = testInfo.getDevice();
+        if (!(testInfo.getDevice() instanceof TestDevice)) {
+            CLog.w("Unexpected type of ITestDevice. Skipping.");
+            return;
+        }
+        TestDevice androidDevice = (TestDevice) testInfo.getDevice();
         sAndroid = new CommandRunner(androidDevice);
 
-        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;
+        // NB: We can't use assumeTrue because the assumption exception is NOT handled by the test
+        // infra when it is thrown from a class method (see b/37502066). We need to skip both here
+        // and in setUp.
+        if (!androidDevice.supportsMicrodroid()) {
+            CLog.i("Microdroid not supported. Skipping.");
             return;
         }
 
@@ -150,11 +148,12 @@
                         .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();
-
-        sMicrodroid = new CommandRunner(microdroidDevice);
     }
 
     @AfterClassWithInfo
@@ -173,7 +172,7 @@
 
     @Before
     public void setUp() throws Exception {
-        assumeFalse(sAssumptionFailed);
+        assumeTrue(((TestDevice) getDevice()).supportsMicrodroid());
         sAndroid.run("mkdir " + TEST_OUTPUT_DIR);
     }
 
@@ -210,11 +209,11 @@
                         + VMADDR_CID_HOST);
 
         // Action
-        String actualHashUnverified4m = computeFileHashOnMicrodroid(MOUNT_DIR + "/6");
-        String actualHash4m = computeFileHashOnMicrodroid(MOUNT_DIR + "/3");
+        String actualHashUnverified4m = computeFileHash(sMicrodroid, MOUNT_DIR + "/6");
+        String actualHash4m = computeFileHash(sMicrodroid, MOUNT_DIR + "/3");
 
         // Verify
-        String expectedHash4m = computeFileHashOnAndroid(TEST_DIR + "/input.4m");
+        String expectedHash4m = computeFileHash(sAndroid, TEST_DIR + "/input.4m");
 
         assertEquals("Inconsistent hash from /authfs/6: ", expectedHash4m, actualHashUnverified4m);
         assertEquals("Inconsistent hash from /authfs/3: ", expectedHash4m, actualHash4m);
@@ -234,12 +233,12 @@
                 + VMADDR_CID_HOST);
 
         // Action
-        String actualHash4k = computeFileHashOnMicrodroid(MOUNT_DIR + "/3");
-        String actualHash4k1 = computeFileHashOnMicrodroid(MOUNT_DIR + "/6");
+        String actualHash4k = computeFileHash(sMicrodroid, MOUNT_DIR + "/3");
+        String actualHash4k1 = computeFileHash(sMicrodroid, MOUNT_DIR + "/6");
 
         // Verify
-        String expectedHash4k = computeFileHashOnAndroid(TEST_DIR + "/input.4k");
-        String expectedHash4k1 = computeFileHashOnAndroid(TEST_DIR + "/input.4k1");
+        String expectedHash4k = computeFileHash(sAndroid, TEST_DIR + "/input.4k");
+        String expectedHash4k1 = computeFileHash(sAndroid, TEST_DIR + "/input.4k1");
 
         assertEquals("Inconsistent hash from /authfs/3: ", expectedHash4k, actualHash4k);
         assertEquals("Inconsistent hash from /authfs/6: ", expectedHash4k1, actualHash4k1);
@@ -254,7 +253,7 @@
         runAuthFsOnMicrodroid("--remote-ro-file 3:" + DIGEST_4M + " --cid " + VMADDR_CID_HOST);
 
         // Verify
-        assertFalse(copyFileOnMicrodroid(MOUNT_DIR + "/3", "/dev/null"));
+        assertThat(copyFile(sMicrodroid, MOUNT_DIR + "/3", "/dev/null")).isFailed();
     }
 
     @Test
@@ -267,10 +266,10 @@
         String srcPath = "/system/bin/linker64";
         String destPath = MOUNT_DIR + "/3";
         String backendPath = TEST_OUTPUT_DIR + "/out.file";
-        assertTrue(copyFileOnMicrodroid(srcPath, destPath));
+        assertThat(copyFile(sMicrodroid, srcPath, destPath)).isSuccess();
 
         // Verify
-        String expectedHash = computeFileHashOnMicrodroid(srcPath);
+        String expectedHash = computeFileHash(sMicrodroid, srcPath);
         expectBackingFileConsistency(destPath, backendPath, expectedHash);
     }
 
@@ -283,30 +282,36 @@
         String srcPath = "/system/bin/linker64";
         String destPath = MOUNT_DIR + "/3";
         String backendPath = TEST_OUTPUT_DIR + "/out.file";
-        assertTrue(copyFileOnMicrodroid(srcPath, destPath));
+        assertThat(copyFile(sMicrodroid, srcPath, destPath)).isSuccess();
 
         // Action
         // Tampering with the first 2 4K-blocks of the backing file.
-        zeroizeFileOnAndroid(backendPath, /* size */ 8192, /* offset */ 0);
+        assertThat(
+                writeZerosAtFileOffset(sAndroid, backendPath,
+                        /* offset */ 0, /* number */ 8192, /* writeThrough */ false))
+                .isSuccess();
 
         // 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));
+        assertThat(
+                writeZerosAtFileOffset(sMicrodroid, destPath,
+                        /* offset */ 0, /* number */ 1024, /* writeThrough */ true))
+                .isFailed();
 
         // 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));
+        assertThat(
+                writeZerosAtFileOffset(sMicrodroid, destPath,
+                        /* offset */ 4096, /* number */ 4096, /* writeThrough */ false))
+                .isSuccess();
 
         // Otherwise, a partial write with correct backing file should still succeed.
-        assertTrue(
-                writeZerosAtFileOffsetOnMicrodroid(
-                        destPath, /* offset */ 8192, /* number */ 1024, /* writeThrough */ false));
+        assertThat(
+                writeZerosAtFileOffset(sMicrodroid, destPath,
+                        /* offset */ 8192, /* number */ 1024, /* writeThrough */ false))
+                .isSuccess();
     }
 
     @Test
@@ -318,20 +323,23 @@
         String srcPath = "/system/bin/linker64";
         String destPath = MOUNT_DIR + "/3";
         String backendPath = TEST_OUTPUT_DIR + "/out.file";
-        assertTrue(copyFileOnMicrodroid(srcPath, destPath));
+        assertThat(copyFile(sMicrodroid, srcPath, destPath)).isSuccess();
 
         // Action
         // Tampering with the first 4K-block of the backing file.
-        zeroizeFileOnAndroid(backendPath, /* size */ 4096, /* offset */ 0);
+        assertThat(
+                writeZerosAtFileOffset(sAndroid, backendPath,
+                        /* offset */ 0, /* number */ 4096, /* writeThrough */ false))
+                .isSuccess();
 
         // 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));
+        assertThat(checkReadAt(sMicrodroid, destPath, /* offset */ 0, /* number */ 4096))
+                .isFailed();
+        assertThat(checkReadAt(sMicrodroid, destPath, /* offset */ 4096, /* number */ 4096))
+                .isSuccess();
     }
 
     @Test
@@ -342,17 +350,20 @@
 
         String outputPath = MOUNT_DIR + "/3";
         String backendPath = TEST_OUTPUT_DIR + "/out.file";
-        createFileWithOnesOnMicrodroid(outputPath, 8192);
+        createFileWithOnes(sMicrodroid, outputPath, 8192);
 
         // Action
         // Tampering with the last 4K-block of the backing file.
-        zeroizeFileOnAndroid(backendPath, /* size */ 1, /* offset */ 4096);
+        assertThat(
+                writeZerosAtFileOffset(sAndroid, backendPath,
+                        /* offset */ 4096, /* number */ 1, /* writeThrough */ false))
+                .isSuccess();
 
         // 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));
+        assertThat(resizeFile(sMicrodroid, outputPath, 8000)).isFailed();
     }
 
     @Test
@@ -364,22 +375,22 @@
         String backendPath = TEST_OUTPUT_DIR + "/out.file";
 
         // Action & Verify
-        createFileWithOnesOnMicrodroid(outputPath, 10000);
-        assertEquals(getFileSizeInBytesOnMicrodroid(outputPath), 10000);
+        createFileWithOnes(sMicrodroid, outputPath, 10000);
+        assertEquals(getFileSizeInBytes(sMicrodroid, outputPath), 10000);
         expectBackingFileConsistency(
                 outputPath,
                 backendPath,
                 "684ad25fdc2bbb80cbc910dd1bde6d5499ccf860ca6ee44704b77ec445271353");
 
-        assertTrue(resizeFileOnMicrodroid(outputPath, 15000));
-        assertEquals(getFileSizeInBytesOnMicrodroid(outputPath), 15000);
+        assertThat(resizeFile(sMicrodroid, outputPath, 15000)).isSuccess();
+        assertEquals(getFileSizeInBytes(sMicrodroid, outputPath), 15000);
         expectBackingFileConsistency(
                 outputPath,
                 backendPath,
                 "567c89f62586e0d33369157afdfe99a2fa36cdffb01e91dcdc0b7355262d610d");
 
-        assertTrue(resizeFileOnMicrodroid(outputPath, 5000));
-        assertEquals(getFileSizeInBytesOnMicrodroid(outputPath), 5000);
+        assertThat(resizeFile(sMicrodroid, outputPath, 5000)).isSuccess();
+        assertEquals(getFileSizeInBytes(sMicrodroid, outputPath), 5000);
         expectBackingFileConsistency(
                 outputPath,
                 backendPath,
@@ -399,16 +410,16 @@
         // Can create a new file to write.
         String expectedAndroidPath = androidOutputDir + "/file";
         String authfsPath = authfsOutputDir + "/file";
-        createFileWithOnesOnMicrodroid(authfsPath, 10000);
-        assertEquals(getFileSizeInBytesOnMicrodroid(authfsPath), 10000);
+        createFileWithOnes(sMicrodroid, authfsPath, 10000);
+        assertEquals(getFileSizeInBytes(sMicrodroid, authfsPath), 10000);
         expectBackingFileConsistency(
                 authfsPath,
                 expectedAndroidPath,
                 "684ad25fdc2bbb80cbc910dd1bde6d5499ccf860ca6ee44704b77ec445271353");
 
         // Regular file operations work, e.g. resize.
-        assertTrue(resizeFileOnMicrodroid(authfsPath, 15000));
-        assertEquals(getFileSizeInBytesOnMicrodroid(authfsPath), 15000);
+        assertThat(resizeFile(sMicrodroid, authfsPath, 15000)).isSuccess();
+        assertEquals(getFileSizeInBytes(sMicrodroid, authfsPath), 15000);
         expectBackingFileConsistency(
                 authfsPath,
                 expectedAndroidPath,
@@ -428,21 +439,21 @@
         // 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);
+        createFileWithOnes(sMicrodroid, authfsOutputDir + "/new_dir/file1", 10000);
+        createFileWithOnes(sMicrodroid, 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);
+        assertEquals(getFileSizeInBytes(sMicrodroid, 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);
+        assertEquals(getFileSizeInBytes(sMicrodroid, authfsOutputDir + "/we/need/file2"), 10000);
         expectBackingFileConsistency(
                 authfsOutputDir + "/we/need/file2",
                 androidOutputDir + "/we/need/file2",
@@ -460,10 +471,10 @@
 
         // Action & Verify
         sMicrodroid.run("echo -n foo > " + authfsOutputDir + "/file");
-        assertEquals(getFileSizeInBytesOnMicrodroid(authfsOutputDir + "/file"), 3);
+        assertEquals(getFileSizeInBytes(sMicrodroid, authfsOutputDir + "/file"), 3);
         // Can override a file and write normally.
-        createFileWithOnesOnMicrodroid(authfsOutputDir + "/file", 10000);
-        assertEquals(getFileSizeInBytesOnMicrodroid(authfsOutputDir + "/file"), 10000);
+        createFileWithOnes(sMicrodroid, authfsOutputDir + "/file", 10000);
+        assertEquals(getFileSizeInBytes(sMicrodroid, authfsOutputDir + "/file"), 10000);
         expectBackingFileConsistency(
                 authfsOutputDir + "/file",
                 androidOutputDir + "/file",
@@ -507,7 +518,7 @@
         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");
+        assertThat(sMicrodroid.runForResult("rmdir " + authfsOutputDir + "/dir")).isFailed();
         sMicrodroid.run("test -d " + authfsOutputDir + "/dir");  // still there
         sMicrodroid.run("rm " + authfsOutputDir + "/dir/file");
         sMicrodroid.run("rmdir " + authfsOutputDir + "/dir");
@@ -531,10 +542,12 @@
 
         // 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");
+        assertThat(sMicrodroid.runForResult("mkdir " + authfsOutputDir + "/some_file")).isFailed();
+        assertThat(sMicrodroid.runForResult("mkdir " + authfsOutputDir + "/some_dir")).isFailed();
+        assertThat(sMicrodroid.runForResult("mkdir " + authfsOutputDir + "/some_dir/file"))
+            .isFailed();
+        assertThat(sMicrodroid.runForResult("mkdir " + authfsOutputDir + "/some_dir/dir"))
+            .isFailed();
     }
 
     @Test
@@ -581,10 +594,10 @@
 
         // Action
         String actualHash =
-                computeFileHashOnMicrodroid(authfsInputDir + "/system/framework/framework.jar");
+                computeFileHash(sMicrodroid, authfsInputDir + "/system/framework/framework.jar");
 
         // Verify
-        String expectedHash = computeFileHashOnAndroid("/system/framework/framework.jar");
+        String expectedHash = computeFileHash(sAndroid, "/system/framework/framework.jar");
         assertEquals("Expect consistent hash through /authfs/3: ", expectedHash, actualHash);
     }
 
@@ -598,7 +611,8 @@
 
         // Verify
         sMicrodroid.run("test -f " + authfsInputDir + "/system/framework/services.jar");
-        assertFailedOnMicrodroid("test -f " + authfsInputDir + "/system/bin/sh");
+        assertThat(sMicrodroid.runForResult("test -f " + authfsInputDir + "/system/bin/sh"))
+                .isFailed();
     }
 
     @Test
@@ -653,8 +667,8 @@
         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");
+        assertThat(sMicrodroid.runForResult("chmod +s " + MOUNT_DIR + "/3")).isFailed();
+        assertThat(sMicrodroid.runForResult("chmod +t " + MOUNT_DIR + "/3")).isFailed();
     }
 
     @Test
@@ -676,8 +690,9 @@
         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");
+        assertThat(sMicrodroid.runForResult("chmod +s " + authfsOutputDir + "/dir/dir2"))
+                .isFailed();
+        assertThat(sMicrodroid.runForResult("chmod +t " + authfsOutputDir + "/dir")).isFailed();
     }
 
     @Test
@@ -699,8 +714,8 @@
         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");
+        assertThat(sMicrodroid.runForResult("chmod +s " + authfsOutputDir + "/file")).isFailed();
+        assertThat(sMicrodroid.runForResult("chmod +t " + authfsOutputDir + "/file2")).isFailed();
     }
 
     @Test
@@ -727,35 +742,17 @@
     private void expectBackingFileConsistency(
             String authFsPath, String backendPath, String expectedHash)
             throws DeviceNotAvailableException {
-        String hashOnAuthFs = computeFileHashOnMicrodroid(authFsPath);
+        String hashOnAuthFs = computeFileHash(sMicrodroid, authFsPath);
         assertEquals("File hash is different to expectation", expectedHash, hashOnAuthFs);
 
-        String hashOfBackingFile = computeFileHashOnAndroid(backendPath);
+        String hashOfBackingFile = computeFileHash(sAndroid, 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)
+    private static String computeFileHash(CommandRunner runner, String path)
             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 result = runner.run("sha256sum " + path);
         String[] tokens = result.split("\\s");
         if (tokens.length > 0) {
             return tokens[0];
@@ -765,6 +762,14 @@
         }
     }
 
+    private static CommandResult copyFile(CommandRunner runner, String src, String dest)
+            throws DeviceNotAvailableException {
+        // toybox's cp(1) implementation ignores most read(2) errors, and it's unclear what the
+        // canonical behavior should be (not mentioned in manpage). For this test, use cat(1) in
+        // order to fail on I/O error.
+        return runner.runForResult("cat " + src + " > " + dest);
+    }
+
     private void expectFileMode(String expected, String microdroidPath, String androidPath)
             throws DeviceNotAvailableException {
         String actual = sMicrodroid.run("stat -c '%A' " + microdroidPath);
@@ -774,35 +779,33 @@
         assertEquals("Inconsistent mode for " + androidPath + " (android)", expected, actual);
     }
 
-    private boolean resizeFileOnMicrodroid(String path, long size)
+    private static CommandResult resizeFile(CommandRunner runner, String path, long size)
             throws DeviceNotAvailableException {
-        CommandResult result = sMicrodroid.runForResult("truncate -c -s " + size + " " + path);
-        return result.getStatus() == CommandStatus.SUCCESS;
+        return runner.runForResult("truncate -c -s " + size + " " + path);
     }
 
-    private long getFileSizeInBytesOnMicrodroid(String path) throws DeviceNotAvailableException {
-        return Long.parseLong(sMicrodroid.run("stat -c '%s' " + path));
+    private static long getFileSizeInBytes(CommandRunner runner, String path)
+            throws DeviceNotAvailableException {
+        return Long.parseLong(runner.run("stat -c '%s' " + path));
     }
 
-    private void createFileWithOnesOnMicrodroid(String filePath, long numberOfOnes)
+    private static void createFileWithOnes(CommandRunner runner, String filePath, long numberOfOnes)
             throws DeviceNotAvailableException {
-        sMicrodroid.run(
+        runner.run(
                 "yes $'\\x01' | tr -d '\\n' | dd bs=1 count=" + numberOfOnes + " of=" + filePath);
     }
 
-    private boolean checkReadAtFileOffsetOnMicrodroid(String filePath, long offset, long size)
-            throws DeviceNotAvailableException {
+    private static CommandResult checkReadAt(CommandRunner runner, 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;
+        return runner.runForResult(cmd);
     }
 
-    private boolean writeZerosAtFileOffsetOnMicrodroid(
-            String filePath, long offset, long numberOfZeros, boolean writeThrough)
-            throws DeviceNotAvailableException {
+    private CommandResult writeZerosAtFileOffset(CommandRunner runner, 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) {
@@ -811,14 +814,7 @@
         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);
+        return runner.runForResult(cmd);
     }
 
     private void runAuthFsOnMicrodroid(String flags) {
diff --git a/compos/aidl/com/android/compos/ICompOsService.aidl b/compos/aidl/com/android/compos/ICompOsService.aidl
index ef48ccf..48a46b1 100644
--- a/compos/aidl/com/android/compos/ICompOsService.aidl
+++ b/compos/aidl/com/android/compos/ICompOsService.aidl
@@ -55,4 +55,11 @@
      * (https://datatracker.ietf.org/doc/html/rfc8032#section-5.1.5).
      */
     byte[] getPublicKey();
+
+    /**
+     * Returns the attestation certificate chain of the current VM. The result is in the form of a
+     * CBOR encoded Boot Certificate Chain (BCC) as defined in
+     * hardware/interfaces/security/dice/aidl/android/hardware/security/dice/Bcc.aidl.
+     */
+    byte[] getAttestationChain();
 }
diff --git a/compos/common/compos_client.rs b/compos/common/compos_client.rs
index 16dc2cf..839280c 100644
--- a/compos/common/compos_client.rs
+++ b/compos/common/compos_client.rs
@@ -64,6 +64,8 @@
     /// Comma separated list of host CPUs where vCPUs are assigned to. If None, any host CPU can be
     /// used to run any vCPU.
     pub cpu_set: Option<String>,
+    /// List of task profiles to apply to the VM
+    pub task_profiles: Vec<String>,
     /// If present, overrides the path to the VM config JSON file
     pub config_path: Option<String>,
     /// If present, overrides the amount of RAM to give the VM
@@ -137,6 +139,7 @@
             memoryMib: parameters.memory_mib.unwrap_or(0), // 0 means use the default
             numCpus: parameters.cpus.map_or(1, NonZeroU32::get) as i32,
             cpuAffinity: parameters.cpu_set.clone(),
+            taskProfiles: parameters.task_profiles.clone(),
         });
 
         let vm = service
diff --git a/compos/compos_key_helper/compos_key_main.cpp b/compos/compos_key_helper/compos_key_main.cpp
index a0d0b18..9ba9f8d 100644
--- a/compos/compos_key_helper/compos_key_main.cpp
+++ b/compos/compos_key_helper/compos_key_main.cpp
@@ -25,6 +25,7 @@
 
 #include "compos_key.h"
 
+using aidl::android::hardware::security::dice::Bcc;
 using aidl::android::hardware::security::dice::BccHandover;
 using aidl::android::hardware::security::dice::InputValues;
 using aidl::android::security::dice::IDiceNode;
@@ -68,6 +69,30 @@
     return 0;
 }
 
+int write_bcc() {
+    ndk::SpAIBinder binder{AServiceManager_getService("android.security.dice.IDiceNode")};
+    auto dice_node = IDiceNode::fromBinder(binder);
+    if (!dice_node) {
+        LOG(ERROR) << "Unable to connect to IDiceNode";
+        return 1;
+    }
+
+    const std::vector<InputValues> empty_input_values;
+    Bcc bcc;
+    auto status = dice_node->getAttestationChain(empty_input_values, &bcc);
+    if (!status.isOk()) {
+        LOG(ERROR) << "GetAttestationChain failed: " << status.getDescription();
+        return 1;
+    }
+
+    if (!WriteFully(STDOUT_FILENO, bcc.data.data(), bcc.data.size())) {
+        PLOG(ERROR) << "Write failed";
+        return 1;
+    }
+
+    return 0;
+}
+
 int sign_input() {
     std::string to_sign;
     if (!ReadFdToString(STDIN_FILENO, &to_sign)) {
@@ -103,6 +128,8 @@
     if (argc == 2) {
         if (argv[1] == "public_key"sv) {
             return write_public_key();
+        } else if (argv[1] == "bcc"sv) {
+            return write_bcc();
         } else if (argv[1] == "sign"sv) {
             return sign_input();
         }
diff --git a/compos/composd/src/instance_manager.rs b/compos/composd/src/instance_manager.rs
index 587314c..60bf20f 100644
--- a/compos/composd/src/instance_manager.rs
+++ b/compos/composd/src/instance_manager.rs
@@ -98,7 +98,14 @@
         }
     };
     let cpu_set = system_properties::read(DEX2OAT_CPU_SET_PROP_NAME)?;
-    Ok(VmParameters { cpus, cpu_set, memory_mib: Some(VM_MEMORY_MIB), ..Default::default() })
+    let task_profiles = vec!["VMCompilationPerformance".to_string()];
+    Ok(VmParameters {
+        cpus,
+        cpu_set,
+        task_profiles,
+        memory_mib: Some(VM_MEMORY_MIB),
+        ..Default::default()
+    })
 }
 
 // Ensures we only run one instance at a time.
diff --git a/compos/composd/src/instance_starter.rs b/compos/composd/src/instance_starter.rs
index 4873d7a..f899497 100644
--- a/compos/composd/src/instance_starter.rs
+++ b/compos/composd/src/instance_starter.rs
@@ -87,7 +87,13 @@
         let _ = fs::remove_file(&self.idsig);
         let _ = fs::remove_file(&self.idsig_manifest_apk);
 
-        self.start_vm(virtualization_service)
+        let instance = self.start_vm(virtualization_service)?;
+
+        // Retrieve the VM's attestation chain as a BCC and save it in the instance directory.
+        let bcc = instance.service.getAttestationChain().context("Getting attestation chain")?;
+        fs::write(self.instance_root.join("bcc"), bcc).context("Writing BCC")?;
+
+        Ok(instance)
     }
 
     fn start_vm(
diff --git a/compos/src/compos_key.rs b/compos/src/compos_key.rs
index eb6248f..faa9d67 100644
--- a/compos/src/compos_key.rs
+++ b/compos/src/compos_key.rs
@@ -21,8 +21,16 @@
 const COMPOS_KEY_HELPER_PATH: &str = "/apex/com.android.compos/bin/compos_key_helper";
 
 pub fn get_public_key() -> Result<Vec<u8>> {
+    get_data_from_helper("public_key")
+}
+
+pub fn get_attestation_chain() -> Result<Vec<u8>> {
+    get_data_from_helper("bcc")
+}
+
+fn get_data_from_helper(command: &str) -> Result<Vec<u8>> {
     let child = Command::new(COMPOS_KEY_HELPER_PATH)
-        .arg("public_key")
+        .arg(command)
         .stdin(Stdio::null())
         .stdout(Stdio::piped())
         .stderr(Stdio::piped())
diff --git a/compos/src/compsvc.rs b/compos/src/compsvc.rs
index 3a794ee..e21aa7d 100644
--- a/compos/src/compsvc.rs
+++ b/compos/src/compsvc.rs
@@ -86,6 +86,10 @@
     fn getPublicKey(&self) -> BinderResult<Vec<u8>> {
         to_binder_result(compos_key::get_public_key())
     }
+
+    fn getAttestationChain(&self) -> BinderResult<Vec<u8>> {
+        to_binder_result(compos_key::get_attestation_chain())
+    }
 }
 
 fn add_artifacts(target_dir: &Path, artifact_signer: &mut ArtifactSigner) -> Result<()> {
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
index 3a2d581..7b5d5ab 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -238,6 +238,9 @@
         parcel.memoryMib = mMemoryMib;
         parcel.numCpus = mNumCpus;
         parcel.cpuAffinity = mCpuAffinity;
+        // Don't allow apps to set task profiles ... at last for now. Also, don't forget to
+        // validate the string because these are appended to the cmdline argument.
+        parcel.taskProfiles = new String[0];
         return parcel;
     }
 
diff --git a/libs/apexutil/Android.bp b/libs/apexutil/Android.bp
new file mode 100644
index 0000000..a1a1ca6
--- /dev/null
+++ b/libs/apexutil/Android.bp
@@ -0,0 +1,40 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_defaults {
+    name: "libapexutil_rust.defaults",
+    crate_name: "apexutil",
+    host_supported: true,
+    srcs: ["src/lib.rs"],
+    prefer_rlib: true,
+    edition: "2018",
+    rustlibs: [
+        "libavb_bindgen",
+        "liblog_rust",
+        "libthiserror",
+        "libzip",
+    ],
+}
+
+rust_library {
+    name: "libapexutil_rust",
+    defaults: ["libapexutil_rust.defaults"],
+}
+
+rust_test {
+    name: "libapexutil_rust.test",
+    defaults: ["libapexutil_rust.defaults"],
+    test_suites: ["general-tests"],
+    data: ["tests/data/*"],
+    target: {
+        host: {
+            // TODO(b/204562227): remove once the build does this automatically
+            data_libs: [
+                "libc++",
+                "libcrypto",
+                "libz",
+            ],
+        },
+    },
+}
diff --git a/libs/apexutil/src/lib.rs b/libs/apexutil/src/lib.rs
new file mode 100644
index 0000000..d53e907
--- /dev/null
+++ b/libs/apexutil/src/lib.rs
@@ -0,0 +1,312 @@
+// Copyright 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.
+
+//! Routines for handling APEX payload
+
+use avb_bindgen::*;
+use std::ffi::{c_void, CStr};
+use std::fs::File;
+use std::io::{self, Read, Seek, SeekFrom};
+use std::mem::{size_of, zeroed};
+use std::ops::Deref;
+use std::ptr::null_mut;
+use std::slice::{from_raw_parts, from_raw_parts_mut};
+use thiserror::Error;
+use zip::result::ZipError;
+use zip::ZipArchive;
+
+const APEX_PUBKEY_ENTRY: &str = "apex_pubkey";
+const APEX_PAYLOAD_ENTRY: &str = "apex_payload.img";
+
+/// Errors from parsing an APEX.
+#[derive(Debug, Error)]
+pub enum ApexParseError {
+    /// There was an IO error.
+    #[error("IO error")]
+    Io(#[from] io::Error),
+    /// The Zip archive was invalid.
+    #[error("Cannot read zip archive")]
+    InvalidZip(&'static str),
+    /// The apex_pubkey file was missing from the APEX.
+    #[error("APEX doesn't contain apex_pubkey")]
+    PubkeyMissing,
+    /// The apex_payload.img file was missing from the APEX.
+    #[error("APEX doesn't contain apex_payload.img")]
+    PayloadMissing,
+    /// The AVB footer in the APEX payload was invalid.
+    #[error("Cannot validate APEX payload AVB footer")]
+    InvalidPayloadAvbFooter,
+    /// There were no descriptors in the APEX payload's AVB footer.
+    #[error("No descriptors found in payload AVB footer")]
+    NoDescriptors,
+    /// There was an invalid descriptor in the APEX payload's AVB footer.
+    #[error("Invalid descriptor found in payload AVB footer")]
+    InvalidDescriptor,
+    /// There was no hashtree descriptor in the APEX payload's AVB footer.
+    #[error("Non-hashtree descriptor found in payload AVB footer")]
+    DescriptorNotHashtree,
+    /// There was an invalid hashtree descriptor in the APEX payload's AVB footer.
+    #[error("Invalid hashtree descriptor found in payload AVB footer")]
+    InvalidHashtreeDescriptor,
+}
+
+/// Errors from verifying an APEX.
+#[derive(Debug, Error)]
+pub enum ApexVerificationError {
+    /// There was an error parsing the APEX.
+    #[error("Cannot parse APEX file")]
+    ParseError(#[from] ApexParseError),
+    /// The APEX payload signature did not validate.
+    #[error("Cannot verify payload signature")]
+    BadPayloadSignature(String),
+    /// The APEX payload was signed with a different key.
+    #[error("Payload is signed with the wrong key")]
+    BadPayloadKey,
+}
+
+/// Verification result holds public key and root digest of apex_payload.img
+pub struct ApexVerificationResult {
+    /// The public key that verifies the payload signature.
+    pub public_key: Vec<u8>,
+    /// The root digest of the payload hashtree.
+    pub root_digest: Vec<u8>,
+}
+
+/// Verify APEX payload by AVB verification and return public key and root digest
+pub fn verify(path: &str) -> Result<ApexVerificationResult, ApexVerificationError> {
+    let apex_file = File::open(path).map_err(ApexParseError::Io)?;
+    let (public_key, image_offset, image_size) = get_public_key_and_image_info(&apex_file)?;
+    let root_digest = verify_vbmeta(apex_file, image_offset, image_size, &public_key)?;
+    Ok(ApexVerificationResult { public_key, root_digest })
+}
+
+fn get_public_key_and_image_info(apex_file: &File) -> Result<(Vec<u8>, u64, u64), ApexParseError> {
+    let mut z = ZipArchive::new(apex_file).map_err(|err| match err {
+        ZipError::Io(err) => ApexParseError::Io(err),
+        ZipError::InvalidArchive(s) | ZipError::UnsupportedArchive(s) => {
+            ApexParseError::InvalidZip(s)
+        }
+        ZipError::FileNotFound => unreachable!(),
+    })?;
+
+    let mut public_key = Vec::new();
+    z.by_name(APEX_PUBKEY_ENTRY)
+        .map_err(|err| match err {
+            ZipError::Io(err) => ApexParseError::Io(err),
+            ZipError::FileNotFound => ApexParseError::PubkeyMissing,
+            ZipError::InvalidArchive(s) | ZipError::UnsupportedArchive(s) => {
+                ApexParseError::InvalidZip(s)
+            }
+        })?
+        .read_to_end(&mut public_key)?;
+
+    let (image_offset, image_size) = z
+        .by_name(APEX_PAYLOAD_ENTRY)
+        .map(|f| (f.data_start(), f.size()))
+        .map_err(|err| match err {
+            ZipError::Io(err) => ApexParseError::Io(err),
+            ZipError::FileNotFound => ApexParseError::PayloadMissing,
+            ZipError::InvalidArchive(s) | ZipError::UnsupportedArchive(s) => {
+                ApexParseError::InvalidZip(s)
+            }
+        })?;
+
+    Ok((public_key, image_offset, image_size))
+}
+
+// Manual addition of a missing enum
+#[allow(non_camel_case_types, dead_code)]
+#[repr(u8)]
+enum AvbDescriptorTag {
+    AVB_DESCRIPTOR_TAG_PROPERTY = 0,
+    AVB_DESCRIPTOR_TAG_HASHTREE,
+    AVB_DESCRIPTOR_TAG_HASH,
+    AVB_DESCRIPTOR_TAG_KERNEL_CMDLINE,
+    AVB_DESCRIPTOR_TAG_CHAIN_PARTITION,
+}
+
+const FOOTER_SIZE: usize = size_of::<AvbFooter>();
+const HASHTREE_DESCRIPTOR_SIZE: usize = size_of::<AvbHashtreeDescriptor>();
+
+/// Verify VBmeta image and return root digest
+fn verify_vbmeta<R: Read + Seek>(
+    image: R,
+    offset: u64,
+    size: u64,
+    public_key: &[u8],
+) -> Result<Vec<u8>, ApexVerificationError> {
+    let vbmeta = VbMeta::from(image, offset, size)?;
+    vbmeta.verify(public_key)?;
+    for &descriptor in vbmeta.descriptors()?.iter() {
+        if let Ok(hashtree_descriptor) = HashtreeDescriptor::from(descriptor) {
+            return Ok(hashtree_descriptor.root_digest());
+        }
+    }
+    Err(ApexParseError::DescriptorNotHashtree.into())
+}
+
+struct VbMeta {
+    data: Vec<u8>,
+}
+
+impl VbMeta {
+    // Read a VbMeta data from a given image
+    fn from<R: Read + Seek>(
+        mut image: R,
+        offset: u64,
+        size: u64,
+    ) -> Result<VbMeta, ApexParseError> {
+        // Get AvbFooter first
+        image.seek(SeekFrom::Start(offset + size - FOOTER_SIZE as u64))?;
+        // SAFETY: AvbDescriptor is a "repr(C,packed)" struct from bindgen
+        let mut footer: AvbFooter = unsafe { zeroed() };
+        // SAFETY: safe to read because of seek(-FOOTER_SIZE) above
+        let avb_footer_valid = unsafe {
+            let footer_slice = from_raw_parts_mut(&mut footer as *mut _ as *mut u8, FOOTER_SIZE);
+            image.read_exact(footer_slice)?;
+            avb_footer_validate_and_byteswap(&footer, &mut footer)
+        };
+        if !avb_footer_valid {
+            return Err(ApexParseError::InvalidPayloadAvbFooter);
+        }
+        // Get VbMeta block
+        image.seek(SeekFrom::Start(offset + footer.vbmeta_offset))?;
+        let vbmeta_size = footer.vbmeta_size as usize;
+        let mut data = vec![0u8; vbmeta_size];
+        image.read_exact(&mut data)?;
+        Ok(VbMeta { data })
+    }
+    // Verify VbMeta image. Its enclosed public key should match with a given public key.
+    fn verify(&self, outer_public_key: &[u8]) -> Result<(), ApexVerificationError> {
+        // SAFETY: self.data points to a valid VBMeta data and avb_vbmeta_image_verify should work fine
+        // with it
+        let public_key = unsafe {
+            let mut pk_ptr: *const u8 = null_mut();
+            let mut pk_len: usize = 0;
+            let res = avb_vbmeta_image_verify(
+                self.data.as_ptr(),
+                self.data.len(),
+                &mut pk_ptr,
+                &mut pk_len,
+            );
+            if res != AvbVBMetaVerifyResult_AVB_VBMETA_VERIFY_RESULT_OK {
+                return Err(ApexVerificationError::BadPayloadSignature(
+                    CStr::from_ptr(avb_vbmeta_verify_result_to_string(res))
+                        .to_string_lossy()
+                        .into_owned(),
+                ));
+            }
+            from_raw_parts(pk_ptr, pk_len)
+        };
+
+        if public_key != outer_public_key {
+            return Err(ApexVerificationError::BadPayloadKey);
+        }
+        Ok(())
+    }
+    // Return a slice of AvbDescriptor pointers
+    fn descriptors(&self) -> Result<Descriptors, ApexParseError> {
+        let mut num: usize = 0;
+        // SAFETY: ptr will be freed by Descriptor.
+        Ok(unsafe {
+            let ptr = avb_descriptor_get_all(self.data.as_ptr(), self.data.len(), &mut num);
+            if ptr.is_null() {
+                return Err(ApexParseError::NoDescriptors);
+            }
+            let all = from_raw_parts(ptr, num);
+            Descriptors { ptr, all }
+        })
+    }
+}
+
+struct HashtreeDescriptor {
+    ptr: *const u8,
+    inner: AvbHashtreeDescriptor,
+}
+
+impl HashtreeDescriptor {
+    fn from(descriptor: *const AvbDescriptor) -> Result<HashtreeDescriptor, ApexParseError> {
+        // SAFETY: AvbDescriptor is a "repr(C,packed)" struct from bindgen
+        let mut desc: AvbDescriptor = unsafe { zeroed() };
+        // SAFETY: both points to valid AvbDescriptor pointers
+        if !unsafe { avb_descriptor_validate_and_byteswap(descriptor, &mut desc) } {
+            return Err(ApexParseError::InvalidDescriptor);
+        }
+        if desc.tag != AvbDescriptorTag::AVB_DESCRIPTOR_TAG_HASHTREE as u64 {
+            return Err(ApexParseError::DescriptorNotHashtree);
+        }
+        // SAFETY: AvbHashtreeDescriptor is a "repr(C, packed)" struct from bindgen
+        let mut hashtree_descriptor: AvbHashtreeDescriptor = unsafe { zeroed() };
+        // SAFETY: With tag == AVB_DESCRIPTOR_TAG_HASHTREE, descriptor should point to
+        // a AvbHashtreeDescriptor.
+        if !unsafe {
+            avb_hashtree_descriptor_validate_and_byteswap(
+                descriptor as *const AvbHashtreeDescriptor,
+                &mut hashtree_descriptor,
+            )
+        } {
+            return Err(ApexParseError::InvalidHashtreeDescriptor);
+        }
+        Ok(Self { ptr: descriptor as *const u8, inner: hashtree_descriptor })
+    }
+    fn root_digest(&self) -> Vec<u8> {
+        // SAFETY: digest_ptr should point to a valid buffer of root_digest_len
+        let root_digest = unsafe {
+            let digest_ptr = self.ptr.offset(
+                HASHTREE_DESCRIPTOR_SIZE as isize
+                    + self.inner.partition_name_len as isize
+                    + self.inner.salt_len as isize,
+            );
+            from_raw_parts(digest_ptr, self.inner.root_digest_len as usize)
+        };
+        root_digest.to_owned()
+    }
+}
+
+// Wraps pointer to a heap-allocated array of AvbDescriptor pointers
+struct Descriptors<'a> {
+    ptr: *mut *const AvbDescriptor,
+    all: &'a [*const AvbDescriptor],
+}
+
+// Wrapped pointer should be freed with avb_free.
+impl Drop for Descriptors<'_> {
+    fn drop(&mut self) {
+        // SAFETY: ptr is allocated by avb_descriptor_get_all
+        unsafe { avb_free(self.ptr as *mut c_void) }
+    }
+}
+
+impl<'a> Deref for Descriptors<'a> {
+    type Target = &'a [*const AvbDescriptor];
+    fn deref(&self) -> &Self::Target {
+        &self.all
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    fn to_hex_string(buf: &[u8]) -> String {
+        buf.iter().map(|b| format!("{:02x}", b)).collect()
+    }
+    #[test]
+    fn test_open_apex() {
+        let res = verify("tests/data/test.apex").unwrap();
+        assert_eq!(
+            to_hex_string(&res.root_digest),
+            "fe11ab17da0a3a738b54bdc3a13f6139cbdf91ec32f001f8d4bbbf8938e04e39"
+        );
+    }
+}
diff --git a/microdroid_manager/tests/data/README.md b/libs/apexutil/tests/data/README.md
similarity index 100%
rename from microdroid_manager/tests/data/README.md
rename to libs/apexutil/tests/data/README.md
diff --git a/microdroid_manager/tests/data/test.apex b/libs/apexutil/tests/data/test.apex
similarity index 100%
rename from microdroid_manager/tests/data/test.apex
rename to libs/apexutil/tests/data/test.apex
Binary files differ
diff --git a/microdroid/Android.bp b/microdroid/Android.bp
index a2ae144..8702568 100644
--- a/microdroid/Android.bp
+++ b/microdroid/Android.bp
@@ -157,16 +157,34 @@
 
 genrule {
     name: "microdroid_build_prop_gen_x86_64",
-    srcs: ["build.prop"],
+    srcs: [
+        "build.prop",
+        ":buildinfo.prop",
+    ],
     out: ["build.prop.out"],
-    cmd: "cp $(in) $(out); echo ro.product.cpu.abilist=x86_64 >> $(out)",
+    cmd: "(echo '# build properties from buildinfo.prop module' && " +
+        "grep ro\\.build\\.version\\.codename= $(location :buildinfo.prop) && " +
+        "grep ro\\.build\\.version\\.release= $(location :buildinfo.prop) && " +
+        "grep ro\\.build\\.version\\.sdk= $(location :buildinfo.prop) && " +
+        "grep ro\\.build\\.version\\.security_patch= $(location :buildinfo.prop) && " +
+        "cat $(location build.prop) && " +
+        "echo ro.product.cpu.abilist=x86_64) > $(out)",
 }
 
 genrule {
     name: "microdroid_build_prop_gen_arm64",
-    srcs: ["build.prop"],
+    srcs: [
+        "build.prop",
+        ":buildinfo.prop",
+    ],
     out: ["build.prop.out"],
-    cmd: "cp $(in) $(out); echo ro.product.cpu.abilist=arm64-v8a >> $(out)",
+    cmd: "(echo '# build properties from buildinfo.prop module' && " +
+        "grep ro\\.build\\.version\\.codename= $(location :buildinfo.prop) && " +
+        "grep ro\\.build\\.version\\.release= $(location :buildinfo.prop) && " +
+        "grep ro\\.build\\.version\\.sdk= $(location :buildinfo.prop) && " +
+        "grep ro\\.build\\.version\\.security_patch= $(location :buildinfo.prop) && " +
+        "cat $(location build.prop) && " +
+        "echo ro.product.cpu.abilist=arm64-v8a) > $(out)",
 }
 
 android_filesystem {
diff --git a/microdroid/build.prop b/microdroid/build.prop
index 2caadbf..a9824c0 100644
--- a/microdroid/build.prop
+++ b/microdroid/build.prop
@@ -3,11 +3,5 @@
 ro.adb.secure=0
 service.adb.listen_addrs=vsock:5555
 
-# TODO(b/189164487): support build related properties
-ro.build.version.codename=Tiramisu
-ro.build.version.release=13
-ro.build.version.sdk=33
-ro.build.version.security_patch=2022-06-05
-
 # Payload metadata partition
 apexd.payload_metadata.path=/dev/block/by-name/payload-metadata
diff --git a/microdroid/payload/mk_payload.cc b/microdroid/payload/mk_payload.cc
index 6e3f526..4dbcabf 100644
--- a/microdroid/payload/mk_payload.cc
+++ b/microdroid/payload/mk_payload.cc
@@ -166,11 +166,10 @@
     Metadata metadata;
     metadata.set_version(1);
 
-    int apex_index = 0;
     for (const auto& apex_config : config.apexes) {
         auto* apex = metadata.add_apexes();
         apex->set_name(apex_config.name);
-        apex->set_partition_name("microdroid-apex-" + std::to_string(apex_index++));
+        apex->set_partition_name(apex_config.name);
         apex->set_is_factory(true);
     }
 
@@ -303,4 +302,4 @@
     }
 
     return 0;
-}
\ No newline at end of file
+}
diff --git a/microdroid_manager/Android.bp b/microdroid_manager/Android.bp
index e4827aa..203e889 100644
--- a/microdroid_manager/Android.bp
+++ b/microdroid_manager/Android.bp
@@ -14,8 +14,8 @@
         "android.system.virtualizationservice-rust",
         "android.system.virtualmachineservice-rust",
         "libanyhow",
+        "libapexutil_rust",
         "libapkverify",
-        "libavb_bindgen",
         "libbinder_rpc_unstable_bindgen",
         "libbinder_rs",
         "libbyteorder",
@@ -40,7 +40,6 @@
         "libuuid",
         "libvsock",
         "librand",
-        "libzip",
     ],
     shared_libs: [
         "libbinder_rpc_unstable",
@@ -71,5 +70,4 @@
             enabled: false,
         },
     },
-    data: ["tests/data/*"],
 }
diff --git a/microdroid_manager/src/main.rs b/microdroid_manager/src/main.rs
index 8a638db..f9b4cf7 100644
--- a/microdroid_manager/src/main.rs
+++ b/microdroid_manager/src/main.rs
@@ -282,6 +282,7 @@
         config.task.is_some(),
         MicrodroidError::InvalidConfig("No task in VM config".to_string())
     );
+    system_properties::write("dev.bootcomplete", "1").context("set dev.bootcomplete")?;
     exec_task(&config.task.unwrap(), service)
 }
 
diff --git a/microdroid_manager/src/payload.rs b/microdroid_manager/src/payload.rs
index 48535f3..a553ce4 100644
--- a/microdroid_manager/src/payload.rs
+++ b/microdroid_manager/src/payload.rs
@@ -14,12 +14,10 @@
 
 //! Routines for handling payload
 
-mod apex;
-
 use crate::instance::ApexData;
 use crate::ioutil::wait_for_file;
 use anyhow::Result;
-use apex::verify;
+use apexutil::verify;
 use log::info;
 use microdroid_metadata::{read_metadata, ApexPayload, Metadata};
 use std::time::Duration;
diff --git a/microdroid_manager/src/payload/apex.rs b/microdroid_manager/src/payload/apex.rs
deleted file mode 100644
index 24c4f05..0000000
--- a/microdroid_manager/src/payload/apex.rs
+++ /dev/null
@@ -1,225 +0,0 @@
-// Copyright 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.
-
-//! Routines for handling APEX payload
-
-use anyhow::{anyhow, ensure, Result};
-use avb_bindgen::*;
-use std::ffi::{c_void, CStr};
-use std::fs::File;
-use std::io::{Read, Seek, SeekFrom};
-use std::mem::{size_of, zeroed};
-use std::ops::Deref;
-use std::ptr::null_mut;
-use std::slice::{from_raw_parts, from_raw_parts_mut};
-use zip::ZipArchive;
-
-const APEX_PUBKEY_ENTRY: &str = "apex_pubkey";
-const APEX_PAYLOAD_ENTRY: &str = "apex_payload.img";
-
-/// Verification result holds public key and root digest of apex_payload.img
-pub struct ApexVerificationResult {
-    pub public_key: Vec<u8>,
-    pub root_digest: Vec<u8>,
-}
-
-/// Verify APEX payload by AVB verification and return public key and root digest
-pub fn verify(path: &str) -> Result<ApexVerificationResult> {
-    let apex_file = File::open(path)?;
-    let (public_key, image_offset, image_size) = get_public_key_and_image_info(&apex_file)?;
-    let root_digest = verify_vbmeta(apex_file, image_offset, image_size, &public_key)?;
-    Ok(ApexVerificationResult { public_key, root_digest })
-}
-
-fn get_public_key_and_image_info(apex_file: &File) -> Result<(Vec<u8>, u64, u64)> {
-    let mut z = ZipArchive::new(apex_file)?;
-
-    let mut public_key = Vec::new();
-    z.by_name(APEX_PUBKEY_ENTRY)?.read_to_end(&mut public_key)?;
-
-    let (image_offset, image_size) =
-        z.by_name(APEX_PAYLOAD_ENTRY).map(|f| (f.data_start(), f.size()))?;
-
-    Ok((public_key, image_offset, image_size))
-}
-
-// Manual addition of a missing enum
-#[allow(non_camel_case_types, dead_code)]
-#[repr(u8)]
-enum AvbDescriptorTag {
-    AVB_DESCRIPTOR_TAG_PROPERTY = 0,
-    AVB_DESCRIPTOR_TAG_HASHTREE,
-    AVB_DESCRIPTOR_TAG_HASH,
-    AVB_DESCRIPTOR_TAG_KERNEL_CMDLINE,
-    AVB_DESCRIPTOR_TAG_CHAIN_PARTITION,
-}
-
-const FOOTER_SIZE: usize = size_of::<AvbFooter>();
-const HASHTREE_DESCRIPTOR_SIZE: usize = size_of::<AvbHashtreeDescriptor>();
-
-/// Verify VBmeta image and return root digest
-fn verify_vbmeta<R: Read + Seek>(
-    image: R,
-    offset: u64,
-    size: u64,
-    public_key: &[u8],
-) -> Result<Vec<u8>> {
-    let vbmeta = VbMeta::from(image, offset, size)?;
-    vbmeta.verify(public_key)?;
-    for &descriptor in vbmeta.descriptors()?.iter() {
-        if let Ok(hashtree_descriptor) = HashtreeDescriptor::from(descriptor) {
-            return hashtree_descriptor.root_digest();
-        }
-    }
-    Err(anyhow!("HashtreeDescriptor is not found."))
-}
-
-struct VbMeta {
-    data: Vec<u8>,
-}
-
-impl VbMeta {
-    // Read a VbMeta data from a given image
-    fn from<R: Read + Seek>(mut image: R, offset: u64, size: u64) -> Result<VbMeta> {
-        // Get AvbFooter first
-        image.seek(SeekFrom::Start(offset + size - FOOTER_SIZE as u64))?;
-        // SAFETY: AvbDescriptor is a "repr(C,packed)" struct from bindgen
-        let mut footer: AvbFooter = unsafe { zeroed() };
-        // SAFETY: safe to read because of seek(-FOOTER_SIZE) above
-        unsafe {
-            let footer_slice = from_raw_parts_mut(&mut footer as *mut _ as *mut u8, FOOTER_SIZE);
-            image.read_exact(footer_slice)?;
-            ensure!(avb_footer_validate_and_byteswap(&footer, &mut footer));
-        }
-        // Get VbMeta block
-        image.seek(SeekFrom::Start(offset + footer.vbmeta_offset))?;
-        let vbmeta_size = footer.vbmeta_size as usize;
-        let mut data = vec![0u8; vbmeta_size];
-        image.read_exact(&mut data)?;
-        Ok(VbMeta { data })
-    }
-    // Verify VbMeta image. Its enclosed public key should match with a given public key.
-    fn verify(&self, outer_public_key: &[u8]) -> Result<()> {
-        // SAFETY: self.data points to a valid VBMeta data and avb_vbmeta_image_verify should work fine
-        // with it
-        let public_key = unsafe {
-            let mut pk_ptr: *const u8 = null_mut();
-            let mut pk_len: usize = 0;
-            let res = avb_vbmeta_image_verify(
-                self.data.as_ptr(),
-                self.data.len(),
-                &mut pk_ptr,
-                &mut pk_len,
-            );
-            ensure!(
-                res == AvbVBMetaVerifyResult_AVB_VBMETA_VERIFY_RESULT_OK,
-                CStr::from_ptr(avb_vbmeta_verify_result_to_string(res))
-                    .to_string_lossy()
-                    .into_owned()
-            );
-            from_raw_parts(pk_ptr, pk_len)
-        };
-
-        ensure!(public_key == outer_public_key, "Public key mismatch with a given one.");
-        Ok(())
-    }
-    // Return a slice of AvbDescriptor pointers
-    fn descriptors(&self) -> Result<Descriptors> {
-        let mut num: usize = 0;
-        // SAFETY: ptr will be freed by Descriptor.
-        Ok(unsafe {
-            let ptr = avb_descriptor_get_all(self.data.as_ptr(), self.data.len(), &mut num);
-            ensure!(!ptr.is_null(), "VbMeta has no descriptors.");
-            let all = from_raw_parts(ptr, num);
-            Descriptors { ptr, all }
-        })
-    }
-}
-
-struct HashtreeDescriptor {
-    ptr: *const u8,
-    inner: AvbHashtreeDescriptor,
-}
-
-impl HashtreeDescriptor {
-    fn from(descriptor: *const AvbDescriptor) -> Result<HashtreeDescriptor> {
-        // SAFETY: AvbDescriptor is a "repr(C,packed)" struct from bindgen
-        let mut desc: AvbDescriptor = unsafe { zeroed() };
-        // SAFETY: both points to valid AvbDescriptor pointers
-        unsafe {
-            ensure!(avb_descriptor_validate_and_byteswap(descriptor, &mut desc));
-        }
-        ensure!({ desc.tag } == AvbDescriptorTag::AVB_DESCRIPTOR_TAG_HASHTREE as u64);
-        // SAFETY: AvbHashtreeDescriptor is a "repr(C, packed)" struct from bindgen
-        let mut hashtree_descriptor: AvbHashtreeDescriptor = unsafe { zeroed() };
-        // SAFETY: With tag == AVB_DESCRIPTOR_TAG_HASHTREE, descriptor should point to
-        // a AvbHashtreeDescriptor.
-        unsafe {
-            ensure!(avb_hashtree_descriptor_validate_and_byteswap(
-                descriptor as *const AvbHashtreeDescriptor,
-                &mut hashtree_descriptor,
-            ));
-        }
-        Ok(Self { ptr: descriptor as *const u8, inner: hashtree_descriptor })
-    }
-    fn root_digest(&self) -> Result<Vec<u8>> {
-        // SAFETY: digest_ptr should point to a valid buffer of root_digest_len
-        let root_digest = unsafe {
-            let digest_ptr = self.ptr.offset(
-                HASHTREE_DESCRIPTOR_SIZE as isize
-                    + self.inner.partition_name_len as isize
-                    + self.inner.salt_len as isize,
-            );
-            from_raw_parts(digest_ptr, self.inner.root_digest_len as usize)
-        };
-        Ok(root_digest.to_owned())
-    }
-}
-
-// Wraps pointer to a heap-allocated array of AvbDescriptor pointers
-struct Descriptors<'a> {
-    ptr: *mut *const AvbDescriptor,
-    all: &'a [*const AvbDescriptor],
-}
-
-// Wrapped pointer should be freed with avb_free.
-impl Drop for Descriptors<'_> {
-    fn drop(&mut self) {
-        // SAFETY: ptr is allocated by avb_descriptor_get_all
-        unsafe { avb_free(self.ptr as *mut c_void) }
-    }
-}
-
-impl<'a> Deref for Descriptors<'a> {
-    type Target = &'a [*const AvbDescriptor];
-    fn deref(&self) -> &Self::Target {
-        &self.all
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    fn to_hex_string(buf: &[u8]) -> String {
-        buf.iter().map(|b| format!("{:02x}", b)).collect()
-    }
-    #[test]
-    fn test_open_apex() {
-        let res = verify("tests/data/test.apex").unwrap();
-        assert_eq!(
-            to_hex_string(&res.root_digest),
-            "fe11ab17da0a3a738b54bdc3a13f6139cbdf91ec32f001f8d4bbbf8938e04e39"
-        );
-    }
-}
diff --git a/pvmfw/Android.bp b/pvmfw/Android.bp
index 5dbd4ec..7385288 100644
--- a/pvmfw/Android.bp
+++ b/pvmfw/Android.bp
@@ -13,7 +13,7 @@
         "libcore.rust_sysroot",
     ],
     rustlibs: [
-        "libspin_nostd",
+        "libvmbase",
     ],
     enabled: false,
     target: {
diff --git a/pvmfw/src/exceptions.rs b/pvmfw/src/exceptions.rs
index 2bdcf9c..61f7846 100644
--- a/pvmfw/src/exceptions.rs
+++ b/pvmfw/src/exceptions.rs
@@ -14,61 +14,59 @@
 
 //! Exception handlers.
 
-use crate::console::emergency_write_str;
-use crate::eprintln;
-use crate::psci::system_reset;
 use core::arch::asm;
+use vmbase::{console::emergency_write_str, eprintln, power::reboot};
 
 #[no_mangle]
 extern "C" fn sync_exception_current() {
     emergency_write_str("sync_exception_current\n");
     print_esr();
-    system_reset();
+    reboot();
 }
 
 #[no_mangle]
 extern "C" fn irq_current() {
     emergency_write_str("irq_current\n");
-    system_reset();
+    reboot();
 }
 
 #[no_mangle]
 extern "C" fn fiq_current() {
     emergency_write_str("fiq_current\n");
-    system_reset();
+    reboot();
 }
 
 #[no_mangle]
 extern "C" fn serr_current() {
     emergency_write_str("serr_current\n");
     print_esr();
-    system_reset();
+    reboot();
 }
 
 #[no_mangle]
 extern "C" fn sync_lower() {
     emergency_write_str("sync_lower\n");
     print_esr();
-    system_reset();
+    reboot();
 }
 
 #[no_mangle]
 extern "C" fn irq_lower() {
     emergency_write_str("irq_lower\n");
-    system_reset();
+    reboot();
 }
 
 #[no_mangle]
 extern "C" fn fiq_lower() {
     emergency_write_str("fiq_lower\n");
-    system_reset();
+    reboot();
 }
 
 #[no_mangle]
 extern "C" fn serr_lower() {
     emergency_write_str("serr_lower\n");
     print_esr();
-    system_reset();
+    reboot();
 }
 
 #[inline]
diff --git a/pvmfw/src/main.rs b/pvmfw/src/main.rs
index d38b1e3..3fe3435 100644
--- a/pvmfw/src/main.rs
+++ b/pvmfw/src/main.rs
@@ -17,13 +17,9 @@
 #![no_main]
 #![no_std]
 
-mod console;
 mod exceptions;
-mod psci;
-mod uart;
 
-use core::panic::PanicInfo;
-use psci::{system_off, system_reset};
+use vmbase::{console, power::shutdown, println};
 
 /// Entry point for pVM firmware.
 #[no_mangle]
@@ -31,14 +27,5 @@
     console::init();
     println!("Hello world");
 
-    system_off();
-    #[allow(clippy::empty_loop)]
-    loop {}
-}
-
-#[panic_handler]
-fn panic(info: &PanicInfo) -> ! {
-    eprintln!("{}", info);
-    system_reset();
-    loop {}
+    shutdown();
 }
diff --git a/pvmfw/src/psci.rs b/pvmfw/src/psci.rs
deleted file mode 100644
index 8dcbcaa..0000000
--- a/pvmfw/src/psci.rs
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright 2022, 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.
-
-//! PSCI calls.
-
-const PSCI_SYSTEM_OFF: u32 = 0x84000008;
-const PSCI_SYSTEM_RESET: u32 = 0x84000009;
-const PSCI_SYSTEM_RESET2: u32 = 0x84000012;
-
-pub fn system_off() -> u32 {
-    hvc32(PSCI_SYSTEM_OFF, 0, 0, 0, 0, 0, 0, 0)[0]
-}
-
-pub fn system_reset() -> u32 {
-    hvc32(PSCI_SYSTEM_RESET, 0, 0, 0, 0, 0, 0, 0)[0]
-}
-
-#[allow(unused)]
-pub fn system_reset2(reset_type: u32, cookie: u32) -> u32 {
-    hvc32(PSCI_SYSTEM_RESET2, reset_type, cookie, 0, 0, 0, 0, 0)[0]
-}
-
-/// Make an HVC32 call to the hypervisor, following the SMC Calling Convention version 1.3.
-#[inline(always)]
-#[allow(clippy::too_many_arguments)]
-fn hvc32(
-    function: u32,
-    arg1: u32,
-    arg2: u32,
-    arg3: u32,
-    arg4: u32,
-    arg5: u32,
-    arg6: u32,
-    arg7: u32,
-) -> [u32; 8] {
-    let mut ret = [0; 8];
-
-    #[cfg(target_arch = "aarch64")]
-    unsafe {
-        core::arch::asm!(
-            "hvc #0",
-            inout("w0") function => ret[0],
-            inout("w1") arg1 => ret[1],
-            inout("w2") arg2 => ret[2],
-            inout("w3") arg3 => ret[3],
-            inout("w4") arg4 => ret[4],
-            inout("w5") arg5 => ret[5],
-            inout("w6") arg6 => ret[6],
-            inout("w7") arg7 => ret[7],
-            options(nomem, nostack)
-        )
-    }
-
-    #[cfg(not(target_arch = "aarch64"))]
-    unimplemented!();
-
-    ret
-}
diff --git a/tests/hostside/helper/Android.bp b/tests/hostside/helper/Android.bp
index 4ca0bf0..6ab02f8 100644
--- a/tests/hostside/helper/Android.bp
+++ b/tests/hostside/helper/Android.bp
@@ -6,7 +6,8 @@
     name: "VirtualizationTestHelper",
     srcs: ["java/**/*.java"],
     libs: [
-        "tradefed",
         "compatibility-tradefed",
+        "tradefed",
+        "truth-prebuilt",
     ],
 }
diff --git a/tests/hostside/helper/java/android/virt/test/CommandResultSubject.java b/tests/hostside/helper/java/android/virt/test/CommandResultSubject.java
new file mode 100644
index 0000000..5312f5a
--- /dev/null
+++ b/tests/hostside/helper/java/android/virt/test/CommandResultSubject.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2022 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 android.virt.test;
+
+import static com.google.common.truth.Truth.assertAbout;
+
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.IntegerSubject;
+import com.google.common.truth.StringSubject;
+import com.google.common.truth.Subject;
+
+/**
+ * A <a href="https://github.com/google/truth">Truth</a> subject for {@link CommandResult}.
+ */
+public class CommandResultSubject extends Subject {
+    private final CommandResult mActual;
+
+    public static Factory<CommandResultSubject, CommandResult> command_results() {
+        return CommandResultSubject::new;
+    }
+
+    public static CommandResultSubject assertThat(CommandResult actual) {
+        return assertAbout(command_results()).that(actual);
+    }
+
+    private CommandResultSubject(FailureMetadata metadata, CommandResult actual) {
+        super(metadata, actual);
+        this.mActual = actual;
+    }
+
+    public void isSuccess() {
+        check("isSuccess()").that(mActual.getStatus()).isEqualTo(CommandStatus.SUCCESS);
+    }
+
+    public void isFailed() {
+        check("isFailed()").that(mActual.getStatus()).isEqualTo(CommandStatus.FAILED);
+    }
+
+    public void isTimedOut() {
+        check("isTimedOut()").that(mActual.getStatus()).isEqualTo(CommandStatus.TIMED_OUT);
+    }
+
+    public void isException() {
+        check("isException()").that(mActual.getStatus()).isEqualTo(CommandStatus.EXCEPTION);
+    }
+
+    public IntegerSubject exitCode() {
+        return check("exitCode()").that(mActual.getExitCode());
+    }
+
+    public StringSubject stdoutTrimmed() {
+        return check("stdout()").that(mActual.getStdout().trim());
+    }
+
+    public StringSubject stderrTrimmed() {
+        return check("stderr()").that(mActual.getStderr().trim());
+    }
+}
diff --git a/tests/hostside/helper/java/android/virt/test/LogArchiver.java b/tests/hostside/helper/java/android/virt/test/LogArchiver.java
new file mode 100644
index 0000000..b6cae95
--- /dev/null
+++ b/tests/hostside/helper/java/android/virt/test/LogArchiver.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2022 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 android.virt.test;
+
+import static com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.result.FileInputStreamSource;
+import com.android.tradefed.result.LogDataType;
+
+import java.io.File;
+
+/** A helper class for archiving device log files to the host's tradefed output directory. */
+public abstract class LogArchiver {
+    /** Copy device log (then delete) to a tradefed output directory on the host.
+     *
+     * @param logs A {@link TestLogData} that needs to be owned by the actual test case.
+     * @param device The device to pull the log file from.
+     * @param remotePath The path on the device.
+     * @param localName Local file name to be copied to.
+     */
+    public static void archiveLogThenDelete(TestLogData logs, ITestDevice device, String remotePath,
+            String localName) throws DeviceNotAvailableException {
+        File logFile = device.pullFile(remotePath);
+        if (logFile != null) {
+            logs.addTestLog(localName, LogDataType.TEXT, new FileInputStreamSource(logFile));
+            // Delete to avoid confusing logs from a previous run, just in case.
+            device.deleteFile(remotePath);
+        }
+    }
+}
diff --git a/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java b/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java
index 440ae18..a55ebe1 100644
--- a/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java
+++ b/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java
@@ -16,10 +16,13 @@
 
 package android.virt.test;
 
+import static android.virt.test.CommandResultSubject.assertThat;
+import static android.virt.test.CommandResultSubject.command_results;
+
 import static com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData;
 
-import static org.hamcrest.CoreMatchers.is;
-import static org.junit.Assert.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeTrue;
@@ -29,12 +32,8 @@
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.TestDevice;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.result.FileInputStreamSource;
-import com.android.tradefed.result.LogDataType;
 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
 import com.android.tradefed.util.CommandResult;
-import com.android.tradefed.util.CommandStatus;
 import com.android.tradefed.util.RunUtil;
 
 import java.io.File;
@@ -96,12 +95,7 @@
 
     public static void archiveLogThenDelete(TestLogData logs, ITestDevice device, String remotePath,
             String localName) throws DeviceNotAvailableException {
-        File logFile = device.pullFile(remotePath);
-        if (logFile != null) {
-            logs.addTestLog(localName, LogDataType.TEXT, new FileInputStreamSource(logFile));
-            // Delete to avoid confusing logs from a previous run, just in case.
-            device.deleteFile(remotePath);
-        }
+        LogArchiver.archiveLogThenDelete(logs, device, remotePath, localName);
     }
 
     // Run an arbitrary command in the host side and returns the result
@@ -120,16 +114,17 @@
     private static String runOnHostWithTimeout(long timeoutMillis, String... cmd) {
         assertTrue(timeoutMillis >= 0);
         CommandResult result = RunUtil.getDefault().runTimedCmd(timeoutMillis, cmd);
-        assertThat(result.getStatus(), is(CommandStatus.SUCCESS));
+        assertThat(result).isSuccess();
         return result.getStdout().trim();
     }
 
     // Run a shell command on Microdroid
     public static String runOnMicrodroid(String... cmd) {
         CommandResult result = runOnMicrodroidForResult(cmd);
-        if (result.getStatus() != CommandStatus.SUCCESS) {
-            fail(join(cmd) + " has failed: " + result);
-        }
+        assertWithMessage("microdroid shell cmd `" + join(cmd) + "`")
+                .about(command_results())
+                .that(result)
+                .isSuccess();
         return result.getStdout().trim();
     }
 
@@ -140,23 +135,13 @@
         CommandResult result = RunUtil.getDefault()
                 .runTimedCmdRetry(timeoutMs, 500, attempts,
                         "adb", "-s", MICRODROID_SERIAL, "shell", join(cmd));
-        if (result.getStatus() != CommandStatus.SUCCESS) {
-            fail(join(cmd) + " has failed: " + result);
-        }
+        assertWithMessage("Command `" + cmd + "` has failed")
+                .about(command_results())
+                .that(result)
+                .isSuccess();
         return result.getStdout().trim();
     }
 
-    // Same as runOnMicrodroid, but returns null on error.
-    public static String tryRunOnMicrodroid(String... cmd) {
-        CommandResult result = runOnMicrodroidForResult(cmd);
-        if (result.getStatus() == CommandStatus.SUCCESS) {
-            return result.getStdout().trim();
-        } else {
-            CLog.d(join(cmd) + " has failed (but ok): " + result);
-            return null;
-        }
-    }
-
     public static CommandResult runOnMicrodroidForResult(String... cmd) {
         final long timeoutMs = 30000; // 30 sec. Microdroid is extremely slow on GCE-on-CF.
         return RunUtil.getDefault()
@@ -175,15 +160,15 @@
                                 "pull",
                                 path,
                                 target.getPath());
-        if (result.getStatus() != CommandStatus.SUCCESS) {
-            fail("pulling " + path + " has failed: " + result);
-        }
+        assertWithMessage("pulling " + path + " from microdroid")
+                .about(command_results())
+                .that(result)
+                .isSuccess();
     }
 
     // Asserts the command will fail on Microdroid.
     public static void assertFailedOnMicrodroid(String... cmd) {
-        CommandResult result = runOnMicrodroidForResult(cmd);
-        assertThat(result.getStatus(), is(CommandStatus.FAILED));
+        assertThat(runOnMicrodroidForResult(cmd)).isFailed();
     }
 
     private static String join(String... strs) {
@@ -397,6 +382,8 @@
         }
 
         // Check if it actually booted by reading a sysprop.
-        assertThat(runOnMicrodroid("getprop", "ro.hardware"), is("microdroid"));
+        assertThat(runOnMicrodroidForResult("getprop", "ro.hardware"))
+                .stdoutTrimmed()
+                .isEqualTo("microdroid");
     }
 }
diff --git a/tests/hostside/java/android/virt/test/MicrodroidTestCase.java b/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
index 32bdf3b..b9550bc 100644
--- a/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
+++ b/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
@@ -16,11 +16,16 @@
 
 package android.virt.test;
 
+import static android.virt.test.CommandResultSubject.assertThat;
+import static android.virt.test.CommandResultSubject.command_results;
+
 import static com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData;
 
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.CoreMatchers.is;
-import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertThat;
@@ -34,7 +39,6 @@
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
 import com.android.tradefed.testtype.junit4.DeviceTestRunOptions;
 import com.android.tradefed.util.CommandResult;
-import com.android.tradefed.util.CommandStatus;
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.RunUtil;
 
@@ -101,11 +105,8 @@
                 false);
     }
 
-    // Wait until logd-init starts. The service is one of the last services that are started in
-    // the microdroid boot procedure. Therefore, waiting for the service means that we wait for
-    // the boot to complete. TODO: we need a better marker eventually.
-    private void waitForLogdInit() {
-        tryRunOnMicrodroid("watch -e \"getprop init.svc.logd-reinit | grep '^$'\"");
+    private void waitForBootComplete() {
+        runOnMicrodroidForResult("watch -e \"getprop dev.bootcomplete | grep '^0$'\"");
     }
 
     @Test
@@ -122,7 +123,7 @@
         assertFalse(runDeviceTests(options));
 
         Map<TestDescription, TestResult> results = getLastDeviceRunResults().getTestResults();
-        assertThat(results.size(), is(1));
+        assertThat(results).hasSize(1);
         TestResult result = results.values().toArray(new TestResult[0])[0];
         assertTrue("The test should fail with a permission error",
                 result.getStackTrace()
@@ -161,9 +162,11 @@
                                     String.join(" ", command));
         String out = result.getStdout();
         String err = result.getStderr();
-        assertEquals(
-                "resigning the Virt APEX failed:\n\tout: " + out + "\n\terr: " + err + "\n",
-                CommandStatus.SUCCESS, result.getStatus());
+        assertWithMessage(
+                "resigning the Virt APEX failed:\n\tout: " + out + "\n\terr: " + err + "\n")
+                .about(command_results())
+                .that(result)
+                .isSuccess();
     }
 
     private static <T> void assertThatEventually(long timeoutMillis, Callable<T> callable,
@@ -263,8 +266,8 @@
         // - apk and idsig
         disks.put(new JSONObject().put("writable", false).put("partitions", new JSONArray()
                 .put(newPartition("payload-metadata", payloadMetadataPath))
-                .put(newPartition("microdroid-apex-0", statsdApexPath))
-                .put(newPartition("microdroid-apex-1", adbdApexPath))
+                .put(newPartition("com.android.os.statsd", statsdApexPath))
+                .put(newPartition("com.android.adbd", adbdApexPath))
                 .put(newPartition("microdroid-apk", apkPath))
                 .put(newPartition("microdroid-apk-idsig", idSigPath))));
 
@@ -361,6 +364,10 @@
 
     @Test
     public void testTombstonesAreBeingForwarded() throws Exception {
+        // This test requires rooting. Skip on user builds where rooting is impossible.
+        final String buildType = getDevice().getProperty("ro.build.type");
+        assumeTrue("userdebug".equals(buildType) || "eng".equals(buildType));
+
         // Note this test relies on logcat values being printed by tombstone_transmit on
         // and the reeceiver on host (virtualization_service)
         final String configPath = "assets/vm_config.json"; // path inside the APK
@@ -376,7 +383,7 @@
                         Optional.of(NUM_VCPUS),
                         Optional.of(CPU_AFFINITY));
         adbConnectToMicrodroid(getDevice(), cid);
-        waitForLogdInit();
+        waitForBootComplete();
         runOnMicrodroid("logcat -c");
         // We need root permission to write to /data/tombstones/
         rootMicrodroid();
@@ -409,7 +416,7 @@
                         Optional.of(NUM_VCPUS),
                         Optional.of(CPU_AFFINITY));
         adbConnectToMicrodroid(getDevice(), cid);
-        waitForLogdInit();
+        waitForBootComplete();
         // Test writing to /data partition
         runOnMicrodroid("echo MicrodroidTest > /data/local/tmp/test.txt");
         assertThat(runOnMicrodroid("cat /data/local/tmp/test.txt"), is("MicrodroidTest"));
@@ -458,10 +465,10 @@
                                     "-w",
                                     "-f",
                                     generalPolicyConfFile.getPath());
-            assertEquals(
-                    "neverallow check failed: " + result.getStderr().trim(),
-                    result.getStatus(),
-                    CommandStatus.SUCCESS);
+            assertWithMessage("neverallow check failed: " + result.getStderr().trim())
+                    .about(command_results())
+                    .that(result)
+                    .isSuccess();
         }
 
         shutdownMicrodroid(getDevice(), cid);
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
index c36e561..22b8a94 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
@@ -67,4 +67,9 @@
      * Default is no mask which means a vCPU can run on any host CPU.
      */
     @nullable String cpuAffinity;
+
+    /**
+     * List of task profile names to apply for the VM
+     */
+    String[] taskProfiles;
 }
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
index dfd3bff..83a81a0 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
@@ -63,4 +63,9 @@
      * The format follows SemVer.
      */
     @utf8InCpp String platformVersion;
+
+    /**
+     * List of task profile names to apply for the VM
+     */
+    String[] taskProfiles;
 }
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index a2e856c..41cc4a5 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -467,6 +467,7 @@
             memory_mib: config.memoryMib.try_into().ok().and_then(NonZeroU32::new),
             cpus: config.numCpus.try_into().ok().and_then(NonZeroU32::new),
             cpu_affinity: config.cpuAffinity.clone(),
+            task_profiles: config.taskProfiles.clone(),
             console_fd,
             log_fd,
             indirect_files,
@@ -634,6 +635,7 @@
     vm_config.protectedVm = config.protectedVm;
     vm_config.numCpus = config.numCpus;
     vm_config.cpuAffinity = config.cpuAffinity.clone();
+    vm_config.taskProfiles = config.taskProfiles.clone();
 
     // Microdroid requires an additional payload disk image and the bootconfig partition.
     if os_name == "microdroid" {
diff --git a/virtualizationservice/src/crosvm.rs b/virtualizationservice/src/crosvm.rs
index f1b179e..b184dca 100644
--- a/virtualizationservice/src/crosvm.rs
+++ b/virtualizationservice/src/crosvm.rs
@@ -64,6 +64,7 @@
     pub memory_mib: Option<NonZeroU32>,
     pub cpus: Option<NonZeroU32>,
     pub cpu_affinity: Option<String>,
+    pub task_profiles: Vec<String>,
     pub console_fd: Option<File>,
     pub log_fd: Option<File>,
     pub indirect_files: Vec<File>,
@@ -326,6 +327,10 @@
         command.arg("--cpu-affinity").arg(cpu_affinity);
     }
 
+    if !config.task_profiles.is_empty() {
+        command.arg("--task-profiles").arg(config.task_profiles.join(","));
+    }
+
     // Keep track of what file descriptors should be mapped to the crosvm process.
     let mut preserved_fds = config.indirect_files.iter().map(|file| file.as_raw_fd()).collect();
 
diff --git a/vm/src/main.rs b/vm/src/main.rs
index 80ea9be..8b438b4 100644
--- a/vm/src/main.rs
+++ b/vm/src/main.rs
@@ -92,6 +92,10 @@
         #[structopt(long)]
         cpu_affinity: Option<String>,
 
+        /// Comma separated list of task profile names to apply to the VM
+        #[structopt(long)]
+        task_profiles: Vec<String>,
+
         /// Paths to extra idsig files.
         #[structopt(long = "extra-idsig")]
         extra_idsigs: Vec<PathBuf>,
@@ -118,6 +122,10 @@
         #[structopt(long)]
         cpu_affinity: Option<String>,
 
+        /// Comma separated list of task profile names to apply to the VM
+        #[structopt(long)]
+        task_profiles: Vec<String>,
+
         /// Path to file for VM console output.
         #[structopt(long)]
         console: Option<PathBuf>,
@@ -200,6 +208,7 @@
             mem,
             cpus,
             cpu_affinity,
+            task_profiles,
             extra_idsigs,
         } => command_run_app(
             service,
@@ -215,9 +224,10 @@
             mem,
             cpus,
             cpu_affinity,
+            task_profiles,
             &extra_idsigs,
         ),
-        Opt::Run { config, daemonize, cpus, cpu_affinity, console, log } => {
+        Opt::Run { config, daemonize, cpus, cpu_affinity, task_profiles, console, log } => {
             command_run(
                 service,
                 &config,
@@ -227,6 +237,7 @@
                 /* mem */ None,
                 cpus,
                 cpu_affinity,
+                task_profiles,
             )
         }
         Opt::Stop { cid } => command_stop(service, cid),
diff --git a/vm/src/run.rs b/vm/src/run.rs
index ef38d7d..3d3d703 100644
--- a/vm/src/run.rs
+++ b/vm/src/run.rs
@@ -54,6 +54,7 @@
     mem: Option<u32>,
     cpus: Option<u32>,
     cpu_affinity: Option<String>,
+    task_profiles: Vec<String>,
     extra_idsigs: &[PathBuf],
 ) -> Result<(), Error> {
     let extra_apks = parse_extra_apk_list(apk, config_path)?;
@@ -105,6 +106,7 @@
         memoryMib: mem.unwrap_or(0) as i32, // 0 means use the VM default
         numCpus: cpus.unwrap_or(1) as i32,
         cpuAffinity: cpu_affinity,
+        taskProfiles: task_profiles,
     });
     run(
         service,
@@ -127,6 +129,7 @@
     mem: Option<u32>,
     cpus: Option<u32>,
     cpu_affinity: Option<String>,
+    task_profiles: Vec<String>,
 ) -> Result<(), Error> {
     let config_file = File::open(config_path).context("Failed to open config file")?;
     let mut config =
@@ -138,6 +141,7 @@
         config.numCpus = cpus as i32;
     }
     config.cpuAffinity = cpu_affinity;
+    config.taskProfiles = task_profiles;
     run(
         service,
         &VirtualMachineConfig::RawConfig(config),
diff --git a/vmbase/Android.bp b/vmbase/Android.bp
new file mode 100644
index 0000000..972cd1b
--- /dev/null
+++ b/vmbase/Android.bp
@@ -0,0 +1,18 @@
+rust_library_rlib {
+    name: "libvmbase",
+    host_supported: false,
+    crate_name: "vmbase",
+    srcs: ["src/lib.rs"],
+    edition: "2021",
+    rustlibs: [
+        "libpsci",
+        "libspin_nostd",
+    ],
+    enabled: false,
+    target: {
+        android_arm64: {
+            enabled: true,
+        },
+    },
+    apex_available: ["com.android.virt"],
+}
diff --git a/pvmfw/src/console.rs b/vmbase/src/console.rs
similarity index 100%
rename from pvmfw/src/console.rs
rename to vmbase/src/console.rs
diff --git a/vmbase/src/lib.rs b/vmbase/src/lib.rs
new file mode 100644
index 0000000..0901e04
--- /dev/null
+++ b/vmbase/src/lib.rs
@@ -0,0 +1,30 @@
+// Copyright 2022, 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.
+
+//! Basic functionality for bare-metal binaries to run in a VM under crosvm.
+
+#![no_std]
+
+pub mod console;
+pub mod power;
+pub mod uart;
+
+use core::panic::PanicInfo;
+use power::reboot;
+
+#[panic_handler]
+fn panic(info: &PanicInfo) -> ! {
+    eprintln!("{}", info);
+    reboot()
+}
diff --git a/vmbase/src/power.rs b/vmbase/src/power.rs
new file mode 100644
index 0000000..10a5e5d
--- /dev/null
+++ b/vmbase/src/power.rs
@@ -0,0 +1,35 @@
+// Copyright 2022, 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.
+
+//! Functions for shutting down the VM.
+
+use psci::{system_off, system_reset};
+
+/// Makes a `PSCI_SYSTEM_OFF` call to shutdown the VM.
+///
+/// Panics if it returns an error.
+pub fn shutdown() -> ! {
+    system_off().unwrap();
+    #[allow(clippy::empty_loop)]
+    loop {}
+}
+
+/// Makes a `PSCI_SYSTEM_RESET` call to shutdown the VM abnormally.
+///
+/// Panics if it returns an error.
+pub fn reboot() -> ! {
+    system_reset().unwrap();
+    #[allow(clippy::empty_loop)]
+    loop {}
+}
diff --git a/pvmfw/src/uart.rs b/vmbase/src/uart.rs
similarity index 100%
rename from pvmfw/src/uart.rs
rename to vmbase/src/uart.rs