Merge "Revert "Increase max VM memory for CompOS""
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 14452a3..3217ee1 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -4,9 +4,6 @@
       "name": "MicrodroidHostTestCases"
     },
     {
-      "name": "ComposHostTestCases"
-    },
-    {
       "name": "MicrodroidTestApp"
     },
     {
@@ -36,6 +33,9 @@
       "name": "ComposBenchmarkApp"
     },
     {
+      "name": "ComposHostTestCases"
+    },
+    {
       "name": "AVFHostTestCases"
     }
   ],
@@ -49,9 +49,6 @@
       "path": "packages/modules/Virtualization/apkdmverity"
     },
     {
-      "path": "packages/modules/Virtualization/avmd"
-    },
-    {
       "path": "packages/modules/Virtualization/encryptedstore"
     },
     {
diff --git a/authfs/Android.bp b/authfs/Android.bp
index 523da35..154a1d6 100644
--- a/authfs/Android.bp
+++ b/authfs/Android.bp
@@ -33,9 +33,6 @@
             enabled: false,
         },
     },
-    shared_libs: [
-        "libbinder_rpc_unstable",
-    ],
     defaults: ["crosvm_defaults"],
 }
 
diff --git a/authfs/fd_server/Android.bp b/authfs/fd_server/Android.bp
index f7cb5e3..5097408 100644
--- a/authfs/fd_server/Android.bp
+++ b/authfs/fd_server/Android.bp
@@ -18,9 +18,6 @@
         "librpcbinder_rs",
     ],
     prefer_rlib: true,
-    shared_libs: [
-        "libbinder_rpc_unstable",
-    ],
     apex_available: ["com.android.virt"],
 }
 
@@ -40,8 +37,5 @@
         "librpcbinder_rs",
     ],
     prefer_rlib: true,
-    shared_libs: [
-        "libbinder_rpc_unstable",
-    ],
     test_suites: ["general-tests"],
 }
diff --git a/authfs/src/file/dir.rs b/authfs/src/file/dir.rs
index f3cc6f8..5d2ec9f 100644
--- a/authfs/src/file/dir.rs
+++ b/authfs/src/file/dir.rs
@@ -28,7 +28,7 @@
 use crate::fsverity::VerifiedFileEditor;
 use crate::fusefs::{AuthFsDirEntry, Inode};
 
-const MAX_ENTRIES: u16 = 100; // Arbitrary limit
+const MAX_ENTRIES: u16 = 1000; // Arbitrary limit
 
 struct InodeInfo {
     inode: Inode,
diff --git a/authfs/tests/benchmarks/Android.bp b/authfs/tests/benchmarks/Android.bp
index 9bdef7b..38ece79 100644
--- a/authfs/tests/benchmarks/Android.bp
+++ b/authfs/tests/benchmarks/Android.bp
@@ -23,7 +23,6 @@
         ":authfs_test_files",
         ":CtsApkVerityTestPrebuiltFiles",
         ":MicrodroidTestApp",
-        ":measure_io",
     ],
 }
 
@@ -36,3 +35,19 @@
         "libbase",
     ],
 }
+
+// Package measure_io binary into a jar, to bundle with the MicrodroidTestApp.
+// When MicrodroidTestApp is mounted inside the Microdroid, the zipfuse will
+// add the +x permission on it.
+java_genrule {
+    name: "measure_io_as_jar",
+    out: ["measure_io.jar"],
+    srcs: [
+        ":measure_io",
+    ],
+    cmd: "out_dir=$$(dirname $(out))" +
+        "&& bin_dir=\"bin\" " +
+        "&& mkdir -p $$out_dir/$$bin_dir" +
+        "&& cp $(in) $$out_dir/$$bin_dir" +
+        "&& jar cf $(out) -C $$out_dir $$bin_dir",
+}
diff --git a/authfs/tests/benchmarks/src/java/com/android/fs/benchmarks/AuthFsBenchmarks.java b/authfs/tests/benchmarks/src/java/com/android/fs/benchmarks/AuthFsBenchmarks.java
index 32eafb8..085d06e 100644
--- a/authfs/tests/benchmarks/src/java/com/android/fs/benchmarks/AuthFsBenchmarks.java
+++ b/authfs/tests/benchmarks/src/java/com/android/fs/benchmarks/AuthFsBenchmarks.java
@@ -18,7 +18,6 @@
 
 import static com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestMetrics;
 
-import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assume.assumeFalse;
 import static org.junit.Assume.assumeTrue;
@@ -45,7 +44,6 @@
 import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.UseParametersRunnerFactory;
 
-import java.io.File;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
@@ -57,11 +55,8 @@
 public class AuthFsBenchmarks extends BaseHostJUnit4Test {
     private static final int TRIAL_COUNT = 5;
 
-    /** Name of the measure_io binary on host. */
-    private static final String MEASURE_IO_BIN_NAME = "measure_io";
-
     /** Path to measure_io on Microdroid. */
-    private static final String MEASURE_IO_BIN_PATH = "/data/local/tmp/measure_io";
+    private static final String MEASURE_IO_BIN_PATH = "/mnt/apk/bin/measure_io";
 
     /** fs-verity digest (sha256) of testdata/input.4m */
     private static final String DIGEST_4M =
@@ -123,7 +118,6 @@
     }
 
     private void readRemoteFile(String mode) throws DeviceNotAvailableException {
-        pushMeasureIoBinToMicrodroid();
         // Cache the file in memory for the host.
         mAuthFsTestRule
                 .getAndroid()
@@ -146,7 +140,6 @@
     }
 
     private void writeRemoteFile(String mode) throws DeviceNotAvailableException {
-        pushMeasureIoBinToMicrodroid();
         String filePath = mAuthFsTestRule.MOUNT_DIR + "/5";
         int fileSizeMb = 8;
         String cmd = MEASURE_IO_BIN_PATH + " " + filePath + " " + fileSizeMb + " " + mode + " w";
@@ -165,14 +158,6 @@
         reportMetrics(rates, mode + "_write", "mb_per_sec");
     }
 
-    private void pushMeasureIoBinToMicrodroid() throws DeviceNotAvailableException {
-        File measureReadBin = mAuthFsTestRule.findTestFile(getBuild(), MEASURE_IO_BIN_NAME);
-        assertThat(measureReadBin.exists()).isTrue();
-        mAuthFsTestRule.getMicrodroidDevice().pushFile(measureReadBin, MEASURE_IO_BIN_PATH);
-        assertThat(mAuthFsTestRule.getMicrodroid().run("ls " + MEASURE_IO_BIN_PATH))
-                .isEqualTo(MEASURE_IO_BIN_PATH);
-    }
-
     private void reportMetrics(List<Double> metrics, String name, String unit) {
         Map<String, Double> stats = mMetricsProcessor.computeStats(metrics, name, unit);
         for (Map.Entry<String, Double> entry : stats.entrySet()) {
diff --git a/avmd/Android.bp b/avmd/Android.bp
deleted file mode 100644
index e5e0553..0000000
--- a/avmd/Android.bp
+++ /dev/null
@@ -1,61 +0,0 @@
-package {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-rust_defaults {
-    name: "libavmd_defaults",
-    crate_name: "avmd",
-    host_supported: true,
-    srcs: ["src/lib.rs"],
-    prefer_rlib: true,
-    rustlibs: [
-        "libhex",
-        "libserde",
-        "libapkverify",
-    ],
-}
-
-rust_library {
-    name: "libavmd",
-    defaults: ["libavmd_defaults"],
-}
-
-rust_defaults {
-    name: "avmdtool.defaults",
-    srcs: ["src/main.rs"],
-    host_supported: true,
-    prefer_rlib: true,
-    rustlibs: [
-        "libanyhow",
-        "libapexutil_rust",
-        "libapkverify",
-        "libavmd",
-        "libclap",
-        "libserde",
-        "libserde_cbor",
-        "libvbmeta_rust",
-    ],
-}
-
-rust_binary {
-    name: "avmdtool",
-    defaults: ["avmdtool.defaults"],
-}
-
-rust_test {
-    name: "avmdtool.test",
-    defaults: ["avmdtool.defaults"],
-    test_suites: ["general-tests"],
-}
-
-rust_test {
-    name: "avmdtool_tests",
-    srcs: ["tests/*_test.rs"],
-    test_suites: ["general-tests"],
-    rustlibs: [
-        "libtempfile",
-    ],
-    compile_multilib: "first",
-    data_bins: ["avmdtool"],
-    data: ["tests/data/*"],
-}
diff --git a/avmd/README.md b/avmd/README.md
deleted file mode 100644
index ae813a0..0000000
--- a/avmd/README.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# The AVMD image format
----
-
-The AVMD image format is used to descibe the verified code that a VM will
-load. This repository contains tools and libraries for working with the AVMD
-image format.
-
-# What is it?
-
-When a VM boots, it loads and verifies a set of images that control execution
-within the VM. Therefore, describing what executes in a VM means describing
-what is loaded. The AVMD image format is designed, for this purpose, to
-describe the closure of images that can be loaded and how they should be
-verified.
-
-# Caveats
-
-The AVMD image format will only allow Android supported signing formats. The
-supported formats are currently limited to [AVB][] and [APK][].
-
-[AVB]: https://android.googlesource.com/platform/external/avb/+/master/README.md
-[APK]: https://source.android.com/security/apksigning#schemes
-
-Verification of the images as they are loaded is the responsibility of the VM.
-The VM is required to only load the images described and to verify them against
-the included parameters. If the VM does not follow this requirement, the
-description of the VM may not be accurate and must not be trusted. Validating
-that the VM behaves as expected requires audit of all boot stages of the VM.
-
-# Using avmdtool
-
-The `.avmd` file can be created as follows
-
-```bash
-avmdtool create /tmp/out.avmd \
-   --vbmeta pvmfw preload u-boot.bin \
-   --vbmeta uboot env_vbmeta disk1/vbmeta.imb \
-   --vbmeta uboot vbmeta micordoid/vbmeta.img \
-   --apk microdroid payload compos.apk \
-   --apk microdroid extra_apk extra_apk.apk \
-   --apex-payload microdroid art_apex art.apex
-```
-
-You can read the `.avmd` file with
-
-```bash
-avmdtool dump /tmp/out.avmd
-```
diff --git a/avmd/TEST_MAPPING b/avmd/TEST_MAPPING
deleted file mode 100644
index 892eb2c..0000000
--- a/avmd/TEST_MAPPING
+++ /dev/null
@@ -1,10 +0,0 @@
-{
-  "avf-presubmit": [
-    {
-      "name": "avmdtool.test"
-    },
-    {
-      "name": "avmdtool_tests"
-    }
-  ]
-}
diff --git a/avmd/src/avmd.rs b/avmd/src/avmd.rs
deleted file mode 100644
index cb02f39..0000000
--- a/avmd/src/avmd.rs
+++ /dev/null
@@ -1,154 +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.
-
-extern crate alloc;
-
-use alloc::{
-    string::{String, ToString},
-    vec::Vec,
-};
-use apkverify::SignatureAlgorithmID;
-use core::fmt;
-use serde::{Deserialize, Serialize};
-
-/// An Avmd struct contains
-/// - A header with version information that allows rollback when needed.
-/// - A list of descriptors that describe different images.
-#[derive(Serialize, Deserialize, Debug, Clone)]
-pub struct Avmd {
-    header: Header,
-    descriptors: Vec<Descriptor>,
-}
-
-impl fmt::Display for Avmd {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        writeln!(f, "Descriptors:")?;
-        for descriptor in &self.descriptors {
-            write!(f, "{}", descriptor)?;
-        }
-        Ok(())
-    }
-}
-
-impl Avmd {
-    /// Creates an instance of Avmd with a given list of descriptors.
-    pub fn new(descriptors: Vec<Descriptor>) -> Avmd {
-        Avmd { header: Header::default(), descriptors }
-    }
-}
-
-static AVMD_MAGIC: u32 = 0x444d5641;
-static AVMD_VERSION_MAJOR: u16 = 1;
-static AVMD_VERSION_MINOR: u16 = 0;
-
-/// Header information for AVMD.
-#[derive(Serialize, Deserialize, Debug, Clone)]
-struct Header {
-    magic: u32,
-    version_major: u16,
-    version_minor: u16,
-}
-
-impl Default for Header {
-    fn default() -> Self {
-        Header {
-            magic: AVMD_MAGIC,
-            version_major: AVMD_VERSION_MAJOR,
-            version_minor: AVMD_VERSION_MINOR,
-        }
-    }
-}
-
-/// AVMD descriptor.
-#[derive(Serialize, Deserialize, Debug, Clone)]
-pub enum Descriptor {
-    /// Descriptor type for the VBMeta images.
-    VbMeta(VbMetaDescriptor),
-    /// Descriptor type for APK.
-    Apk(ApkDescriptor),
-}
-
-impl fmt::Display for Descriptor {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        match self {
-            Descriptor::VbMeta(descriptor) => write!(f, "{}", descriptor),
-            Descriptor::Apk(descriptor) => write!(f, "{}", descriptor),
-        }
-    }
-}
-
-/// VbMeta descriptor.
-#[derive(Serialize, Deserialize, Debug, Clone)]
-pub struct VbMetaDescriptor {
-    /// The identifier of this resource.
-    #[serde(flatten)]
-    pub resource: ResourceIdentifier,
-    /// The SHA-512 [VBMeta digest][] calculated from the top-level VBMeta image.
-    ///
-    /// [VBMeta digest]: https://android.googlesource.com/platform/external/avb/+/master/README.md#the-vbmeta-digest
-    pub vbmeta_digest: Vec<u8>,
-}
-
-impl fmt::Display for VbMetaDescriptor {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        writeln!(f, "  VBMeta descriptor:")?;
-        writeln!(f, "    namespace:             {}", self.resource.namespace)?;
-        writeln!(f, "    name:                  {}", self.resource.name)?;
-        writeln!(f, "    vbmeta digest:         {}", hex::encode(&self.vbmeta_digest))?;
-        Ok(())
-    }
-}
-
-/// APK descriptor.
-#[derive(Serialize, Deserialize, Debug, Clone)]
-pub struct ApkDescriptor {
-    /// The identifier of this resource.
-    #[serde(flatten)]
-    pub resource: ResourceIdentifier,
-    /// The ID of the algoithm used to sign the APK.
-    /// It should be one of the algorithms in the [list][].
-    ///
-    /// [list]: https://source.android.com/security/apksigning/v2#signature-algorithm-ids
-    pub signature_algorithm_id: SignatureAlgorithmID,
-    /// Digest of the APK's v3 signing block. TODO: fix
-    pub apk_digest: Vec<u8>,
-}
-
-impl fmt::Display for ApkDescriptor {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        writeln!(f, "  APK descriptor:")?;
-        writeln!(f, "    namespace:             {}", self.resource.namespace)?;
-        writeln!(f, "    name:                  {}", self.resource.name)?;
-        writeln!(f, "    Signing algorithm ID:  {:#04x}", self.signature_algorithm_id.to_u32())?;
-        writeln!(f, "    APK digest:            {}", hex::encode(&self.apk_digest))?;
-        Ok(())
-    }
-}
-
-/// Resource identifier regroups information to identify resources.
-#[derive(Serialize, Deserialize, Debug, Clone)]
-pub struct ResourceIdentifier {
-    /// Namespace of the resource.
-    namespace: String,
-    /// Name of the resource.
-    name: String,
-}
-
-impl ResourceIdentifier {
-    /// Creates an instance of ResourceIdentifier with the given
-    /// namespace and name.
-    pub fn new(namespace: &str, name: &str) -> ResourceIdentifier {
-        ResourceIdentifier { namespace: namespace.to_string(), name: name.to_string() }
-    }
-}
diff --git a/avmd/src/lib.rs b/avmd/src/lib.rs
deleted file mode 100644
index 7a06e6a..0000000
--- a/avmd/src/lib.rs
+++ /dev/null
@@ -1,21 +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.
-
-//! Library for handling AVMD blobs.
-
-#![no_std]
-
-mod avmd;
-
-pub use avmd::{ApkDescriptor, Avmd, Descriptor, ResourceIdentifier, VbMetaDescriptor};
diff --git a/avmd/src/main.rs b/avmd/src/main.rs
deleted file mode 100644
index 8d7cb57..0000000
--- a/avmd/src/main.rs
+++ /dev/null
@@ -1,176 +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.
-
-//! Tool for handling AVMD blobs.
-
-use anyhow::{anyhow, bail, Result};
-use apexutil::get_payload_vbmeta_image_hash;
-use apkverify::get_apk_digest;
-use avmd::{ApkDescriptor, Avmd, Descriptor, ResourceIdentifier, VbMetaDescriptor};
-use clap::{
-    builder::ValueParser,
-    parser::{Indices, ValuesRef},
-    Arg, ArgAction, ArgMatches, Command,
-};
-use serde::ser::Serialize;
-use std::{fs::File, path::PathBuf};
-use vbmeta::VbMetaImage;
-
-fn get_vbmeta_image_hash(file: &str) -> Result<Vec<u8>> {
-    let img = VbMetaImage::verify_path(file)?;
-    Ok(img.hash().ok_or_else(|| anyhow!("No hash as VBMeta image isn't signed"))?.to_vec())
-}
-
-/// Iterate over a set of argument values, that could be empty or come in
-/// (<index>, <namespace>, <name>, <file>) tuple.
-struct NamespaceNameFileIterator<'a> {
-    indices: Option<Indices<'a>>,
-    values: Option<ValuesRef<'a, String>>,
-}
-
-impl<'a> NamespaceNameFileIterator<'a> {
-    fn new(args: &'a ArgMatches, name: &'a str) -> Self {
-        NamespaceNameFileIterator { indices: args.indices_of(name), values: args.get_many(name) }
-    }
-}
-
-impl<'a> Iterator for NamespaceNameFileIterator<'a> {
-    type Item = (usize, &'a str, &'a str, &'a str);
-
-    fn next(&mut self) -> Option<Self::Item> {
-        match (self.indices.as_mut(), self.values.as_mut()) {
-            (Some(indices), Some(values)) => {
-                match (indices.nth(2), values.next(), values.next(), values.next()) {
-                    (Some(index), Some(namespace), Some(name), Some(file)) => {
-                        Some((index, namespace, name, file))
-                    }
-                    _ => None,
-                }
-            }
-            _ => None,
-        }
-    }
-}
-
-fn create(args: &ArgMatches) -> Result<()> {
-    // Store descriptors in the order they were given in the arguments
-    // TODO: instead, group them by namespace?
-    let mut descriptors = std::collections::BTreeMap::new();
-    for (i, namespace, name, file) in NamespaceNameFileIterator::new(args, "vbmeta") {
-        descriptors.insert(
-            i,
-            Descriptor::VbMeta(VbMetaDescriptor {
-                resource: ResourceIdentifier::new(namespace, name),
-                vbmeta_digest: get_vbmeta_image_hash(file)?,
-            }),
-        );
-    }
-    for (i, namespace, name, file) in NamespaceNameFileIterator::new(args, "apk") {
-        let file = File::open(file)?;
-        let (signature_algorithm_id, apk_digest) = get_apk_digest(file, /*verify=*/ true)?;
-        descriptors.insert(
-            i,
-            Descriptor::Apk(ApkDescriptor {
-                resource: ResourceIdentifier::new(namespace, name),
-                signature_algorithm_id,
-                apk_digest: apk_digest.to_vec(),
-            }),
-        );
-    }
-    for (i, namespace, name, file) in NamespaceNameFileIterator::new(args, "apex-payload") {
-        descriptors.insert(
-            i,
-            Descriptor::VbMeta(VbMetaDescriptor {
-                resource: ResourceIdentifier::new(namespace, name),
-                vbmeta_digest: get_payload_vbmeta_image_hash(file)?,
-            }),
-        );
-    }
-    let avmd = Avmd::new(descriptors.into_values().collect());
-    let mut bytes = Vec::new();
-    avmd.serialize(
-        &mut serde_cbor::Serializer::new(&mut serde_cbor::ser::IoWrite::new(&mut bytes))
-            .packed_format()
-            .legacy_enums(),
-    )?;
-    std::fs::write(args.get_one::<PathBuf>("file").unwrap(), &bytes)?;
-    Ok(())
-}
-
-fn dump(args: &ArgMatches) -> Result<()> {
-    let file = std::fs::read(args.get_one::<PathBuf>("file").unwrap())?;
-    let avmd: Avmd = serde_cbor::from_slice(&file)?;
-    println!("{}", avmd);
-    Ok(())
-}
-
-fn clap_command() -> Command {
-    let namespace_name_file = ["namespace", "name", "file"];
-
-    Command::new("avmdtool")
-        .subcommand_required(true)
-        .arg_required_else_help(true)
-        .subcommand(
-            Command::new("create")
-                .arg_required_else_help(true)
-                .arg(Arg::new("file").value_parser(ValueParser::path_buf()).required(true))
-                .arg(
-                    Arg::new("vbmeta")
-                        .long("vbmeta")
-                        .value_names(namespace_name_file)
-                        .num_args(3)
-                        .action(ArgAction::Append),
-                )
-                .arg(
-                    Arg::new("apk")
-                        .long("apk")
-                        .value_names(namespace_name_file)
-                        .num_args(3)
-                        .action(ArgAction::Append),
-                )
-                .arg(
-                    Arg::new("apex-payload")
-                        .long("apex-payload")
-                        .value_names(namespace_name_file)
-                        .num_args(3)
-                        .action(ArgAction::Append),
-                ),
-        )
-        .subcommand(
-            Command::new("dump")
-                .arg_required_else_help(true)
-                .arg(Arg::new("file").value_parser(ValueParser::path_buf()).required(true)),
-        )
-}
-
-fn main() -> Result<()> {
-    let args = clap_command().get_matches();
-    match args.subcommand() {
-        Some(("create", sub_args)) => create(sub_args)?,
-        Some(("dump", sub_args)) => dump(sub_args)?,
-        _ => bail!("Invalid arguments"),
-    }
-    Ok(())
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn verify_command() {
-        // Check that the command parsing has been configured in a valid way.
-        clap_command().debug_assert();
-    }
-}
diff --git a/avmd/tests/avmdtool_test.rs b/avmd/tests/avmdtool_test.rs
deleted file mode 100644
index 4647f06..0000000
--- a/avmd/tests/avmdtool_test.rs
+++ /dev/null
@@ -1,65 +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.
-
-//! Tests for avmdtool.
-
-use std::fs;
-use std::process::Command;
-use tempfile::TempDir;
-
-#[test]
-fn test_dump() {
-    let filename = "tests/data/test.avmd";
-    assert!(
-        fs::metadata(filename).is_ok(),
-        "File '{}' does not exist. You can re-create it with:
-    avmdtool create {} \\
-    --apex-payload microdroid vbmeta tests/data/test.apex \\
-    --apk microdroid_manager apk \\
-    tests/data/v3-only-with-rsa-pkcs1-sha256-4096.apk \\
-    --apk microdroid_manager extra-apk tests/data/v3-only-with-stamp.apk",
-        filename,
-        filename
-    );
-    let output = Command::new("./avmdtool").args(["dump", filename]).output().unwrap();
-    assert!(output.status.success());
-    assert_eq!(output.stdout, fs::read("tests/data/test.avmd.dump").unwrap());
-}
-
-#[test]
-fn test_create() {
-    let test_dir = TempDir::new().unwrap();
-    let test_file_path = test_dir.path().join("tmp_test.amvd");
-    let output = Command::new("./avmdtool")
-        .args([
-            "create",
-            test_file_path.to_str().unwrap(),
-            "--apex-payload",
-            "microdroid",
-            "vbmeta",
-            "tests/data/test.apex",
-            "--apk",
-            "microdroid_manager",
-            "apk",
-            "tests/data/v3-only-with-rsa-pkcs1-sha256-4096.apk",
-            "--apk",
-            "microdroid_manager",
-            "extra-apk",
-            "tests/data/v3-only-with-stamp.apk",
-        ])
-        .output()
-        .unwrap();
-    assert!(output.status.success());
-    assert_eq!(fs::read(test_file_path).unwrap(), fs::read("tests/data/test.avmd").unwrap());
-}
diff --git a/avmd/tests/data/test.apex b/avmd/tests/data/test.apex
deleted file mode 100644
index fd79365..0000000
--- a/avmd/tests/data/test.apex
+++ /dev/null
Binary files differ
diff --git a/avmd/tests/data/test.avmd b/avmd/tests/data/test.avmd
deleted file mode 100644
index e567125..0000000
--- a/avmd/tests/data/test.avmd
+++ /dev/null
Binary files differ
diff --git a/avmd/tests/data/test.avmd.dump b/avmd/tests/data/test.avmd.dump
deleted file mode 100644
index a63a151..0000000
--- a/avmd/tests/data/test.avmd.dump
+++ /dev/null
@@ -1,16 +0,0 @@
-Descriptors:
-  VBMeta descriptor:
-    namespace:             microdroid
-    name:                  vbmeta
-    vbmeta digest:         296e32a76544de9da01713e471403ab4667705ad527bb4f1fac0cf61e7ce122d
-  APK descriptor:
-    namespace:             microdroid_manager
-    name:                  apk
-    Signing algorithm ID:  0x103
-    APK digest:            0df2426ea33aedaf495d88e5be0c6a1663ff0a81c5ed12d5b2929ae4b4300f2f
-  APK descriptor:
-    namespace:             microdroid_manager
-    name:                  extra-apk
-    Signing algorithm ID:  0x201
-    APK digest:            626bb647c0089717a7ffa52fd8e845f9403d5e27f7a5a8752e47b3345fb82f5c
-
diff --git a/avmd/tests/data/v3-only-with-rsa-pkcs1-sha256-4096.apk b/avmd/tests/data/v3-only-with-rsa-pkcs1-sha256-4096.apk
deleted file mode 100644
index 0c9391c..0000000
--- a/avmd/tests/data/v3-only-with-rsa-pkcs1-sha256-4096.apk
+++ /dev/null
Binary files differ
diff --git a/avmd/tests/data/v3-only-with-stamp.apk b/avmd/tests/data/v3-only-with-stamp.apk
deleted file mode 100644
index 5f65214..0000000
--- a/avmd/tests/data/v3-only-with-stamp.apk
+++ /dev/null
Binary files differ
diff --git a/compos/Android.bp b/compos/Android.bp
index 0890e9d..2f6be98 100644
--- a/compos/Android.bp
+++ b/compos/Android.bp
@@ -27,7 +27,6 @@
     ],
     prefer_rlib: true,
     shared_libs: [
-        "libbinder_rpc_unstable",
         "libcrypto",
     ],
 }
diff --git a/compos/aidl/com/android/compos/ICompOsService.aidl b/compos/aidl/com/android/compos/ICompOsService.aidl
index df8c91e..497c35e 100644
--- a/compos/aidl/com/android/compos/ICompOsService.aidl
+++ b/compos/aidl/com/android/compos/ICompOsService.aidl
@@ -87,7 +87,7 @@
     /**
      * 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.
+     * hardware/interfaces/security/rkp/aidl/android/hardware/security/keymint/ProtectedData.aidl
      */
     byte[] getAttestationChain();
 
diff --git a/compos/common/Android.bp b/compos/common/Android.bp
index 7a7042e..05bc093 100644
--- a/compos/common/Android.bp
+++ b/compos/common/Android.bp
@@ -12,6 +12,7 @@
         "compos_aidl_interface-rust",
         "libanyhow",
         "libbinder_rs",
+        "libglob",
         "liblazy_static",
         "liblog_rust",
         "libnested_virt",
@@ -20,9 +21,6 @@
         "libvmclient",
     ],
     proc_macros: ["libnum_derive"],
-    shared_libs: [
-        "libbinder_rpc_unstable",
-    ],
     apex_available: [
         "com.android.compos",
     ],
diff --git a/compos/common/compos_client.rs b/compos/common/compos_client.rs
index f6811cb..96c8147 100644
--- a/compos/common/compos_client.rs
+++ b/compos/common/compos_client.rs
@@ -22,30 +22,41 @@
     COMPOS_APEX_ROOT, COMPOS_DATA_ROOT, COMPOS_VSOCK_PORT,
 };
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
+    CpuTopology::CpuTopology,
     IVirtualizationService::IVirtualizationService,
     VirtualMachineAppConfig::{DebugLevel::DebugLevel, Payload::Payload, VirtualMachineAppConfig},
     VirtualMachineConfig::VirtualMachineConfig,
 };
-use anyhow::{bail, Context, Result};
+use anyhow::{anyhow, bail, Context, Result};
 use binder::{ParcelFileDescriptor, Strong};
 use compos_aidl_interface::aidl::com::android::compos::ICompOsService::ICompOsService;
+use glob::glob;
 use log::{info, warn};
 use rustutils::system_properties;
-use std::fs::{self, File};
-use std::num::NonZeroU32;
+use std::fs::File;
 use std::path::{Path, PathBuf};
 use vmclient::{DeathReason, ErrorCode, VmInstance, VmWaitError};
 
 /// This owns an instance of the CompOS VM.
 pub struct ComposClient(VmInstance);
 
+/// CPU topology configuration for a virtual machine.
+#[derive(Default, Debug, Clone)]
+pub enum VmCpuTopology {
+    /// Run VM with 1 vCPU only.
+    #[default]
+    OneCpu,
+    /// Run VM vCPU topology matching that of the host.
+    MatchHost,
+}
+
 /// Parameters to be used when creating a virtual machine instance.
 #[derive(Default, Debug, Clone)]
 pub struct VmParameters {
     /// Whether the VM should be debuggable.
     pub debug_mode: bool,
-    /// Number of vCPUs to have in the VM. If None, defaults to 1.
-    pub cpus: Option<NonZeroU32>,
+    /// CPU topology of the VM. Defaults to 1 vCPU.
+    pub cpu_topology: VmCpuTopology,
     /// List of task profiles to apply to the VM
     pub task_profiles: Vec<String>,
     /// If present, overrides the amount of RAM to give the VM
@@ -111,6 +122,11 @@
             (Some(console_fd), Some(log_fd))
         };
 
+        let cpu_topology = match parameters.cpu_topology {
+            VmCpuTopology::OneCpu => CpuTopology::ONE_CPU,
+            VmCpuTopology::MatchHost => CpuTopology::MATCH_HOST,
+        };
+
         let config = VirtualMachineConfig::AppConfig(VirtualMachineAppConfig {
             name: String::from("Compos"),
             apk: Some(apk_fd),
@@ -122,8 +138,9 @@
             extraIdsigs: extra_idsigs,
             protectedVm: protected_vm,
             memoryMib: parameters.memory_mib.unwrap_or(0), // 0 means use the default
-            numCpus: parameters.cpus.map_or(1, NonZeroU32::get) as i32,
+            cpuTopology: cpu_topology,
             taskProfiles: parameters.task_profiles.clone(),
+            gdbPort: 0, // Don't start gdb-server
         });
 
         let callback = Box::new(Callback {});
@@ -178,15 +195,19 @@
     // Our config APK will be in a directory under app, but the name of the directory is at the
     // discretion of the build system. So just look in each sub-directory until we find it.
     // (In practice there will be exactly one directory, so this shouldn't take long.)
-    let app_dir = apex_dir.join("app");
-    for dir in fs::read_dir(app_dir).context("Reading app dir")? {
-        let apk_file = dir?.path().join("CompOSPayloadApp.apk");
-        if apk_file.is_file() {
-            return Ok(apk_file);
-        }
+    let app_glob = apex_dir.join("app").join("**").join("CompOSPayloadApp*.apk");
+    let mut entries: Vec<PathBuf> =
+        glob(app_glob.to_str().ok_or_else(|| anyhow!("Invalid path: {}", app_glob.display()))?)
+            .context("failed to glob")?
+            .filter_map(|e| e.ok())
+            .collect();
+    if entries.len() > 1 {
+        bail!("Found more than one apk matching {}", app_glob.display());
     }
-
-    bail!("Failed to locate CompOSPayloadApp.apk")
+    match entries.pop() {
+        Some(path) => Ok(path),
+        None => Err(anyhow!("No apks match {}", app_glob.display())),
+    }
 }
 
 fn prepare_idsig(
diff --git a/compos/common/lib.rs b/compos/common/lib.rs
index 8d49ff0..1f937c9 100644
--- a/compos/common/lib.rs
+++ b/compos/common/lib.rs
@@ -53,9 +53,6 @@
 /// /system_ext available in CompOS.
 pub const IDSIG_MANIFEST_EXT_APK_FILE: &str = "idsig_manifest_ext_apk";
 
-/// Number of CPUs to run dex2oat (actually the entire compos VM) with
-pub const DEX2OAT_THREADS_PROP_NAME: &str = "dalvik.vm.boot-dex2oat-threads";
-
 /// The Android path of fs-verity build manifest APK for /system.
 pub const BUILD_MANIFEST_APK_PATH: &str = "/system/etc/security/fsverity/BuildManifest.apk";
 
diff --git a/compos/composd/Android.bp b/compos/composd/Android.bp
index 07a9be3..cee4b01 100644
--- a/compos/composd/Android.bp
+++ b/compos/composd/Android.bp
@@ -17,7 +17,6 @@
         "libcompos_common",
         "libcomposd_native_rust",
         "libminijail_rust",
-        "libnum_cpus",
         "libnix",
         "liblibc",
         "liblog_rust",
diff --git a/compos/composd/src/instance_manager.rs b/compos/composd/src/instance_manager.rs
index c3d6592..98d4a1b 100644
--- a/compos/composd/src/instance_manager.rs
+++ b/compos/composd/src/instance_manager.rs
@@ -21,11 +21,8 @@
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice;
 use anyhow::{bail, Result};
 use binder::Strong;
-use compos_common::compos_client::VmParameters;
-use compos_common::{CURRENT_INSTANCE_DIR, DEX2OAT_THREADS_PROP_NAME, TEST_INSTANCE_DIR};
-use rustutils::system_properties;
-use std::num::NonZeroU32;
-use std::str::FromStr;
+use compos_common::compos_client::{VmCpuTopology, VmParameters};
+use compos_common::{CURRENT_INSTANCE_DIR, TEST_INSTANCE_DIR};
 use std::sync::{Arc, Mutex, Weak};
 use virtualizationservice::IVirtualizationService::IVirtualizationService;
 
@@ -79,16 +76,17 @@
 }
 
 fn new_vm_parameters() -> Result<VmParameters> {
-    let cpus = match system_properties::read(DEX2OAT_THREADS_PROP_NAME)? {
-        Some(s) => Some(NonZeroU32::from_str(&s)?),
-        None => {
-            // dex2oat uses all CPUs by default. To match the behavior, give the VM all CPUs by
-            // default.
-            NonZeroU32::new(num_cpus::get() as u32)
-        }
-    };
+    // By default, dex2oat starts as many threads as there are CPUs. This can be overridden with
+    // a system property. Start the VM with all CPUs and assume the guest will start a suitable
+    // number of dex2oat threads.
+    let cpu_topology = VmCpuTopology::MatchHost;
     let task_profiles = vec!["SCHED_SP_COMPUTE".to_string()];
-    Ok(VmParameters { cpus, task_profiles, memory_mib: Some(VM_MEMORY_MIB), ..Default::default() })
+    Ok(VmParameters {
+        cpu_topology,
+        task_profiles,
+        memory_mib: Some(VM_MEMORY_MIB),
+        ..Default::default()
+    })
 }
 
 // Ensures we only run one instance at a time.
diff --git a/compos/composd_cmd/Android.bp b/compos/composd_cmd/Android.bp
index 54b0bad..77caad8 100644
--- a/compos/composd_cmd/Android.bp
+++ b/compos/composd_cmd/Android.bp
@@ -2,8 +2,8 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-rust_binary {
-    name: "composd_cmd",
+rust_defaults {
+    name: "composd_cmd_defaults",
     srcs: ["composd_cmd.rs"],
     edition: "2021",
     rustlibs: [
@@ -12,8 +12,14 @@
         "libbinder_rs",
         "libclap",
         "libcompos_common",
+        "libhypervisor_props",
     ],
     prefer_rlib: true,
+}
+
+rust_binary {
+    name: "composd_cmd",
+    defaults: ["composd_cmd_defaults"],
     apex_available: [
         "com.android.compos",
     ],
@@ -21,15 +27,6 @@
 
 rust_test {
     name: "composd_cmd.test",
-    srcs: ["composd_cmd.rs"],
-    edition: "2021",
-    rustlibs: [
-        "android.system.composd-rust",
-        "libanyhow",
-        "libbinder_rs",
-        "libclap",
-        "libcompos_common",
-    ],
-    prefer_rlib: true,
+    defaults: ["composd_cmd_defaults"],
     test_suites: ["general-tests"],
 }
diff --git a/compos/composd_cmd/composd_cmd.rs b/compos/composd_cmd/composd_cmd.rs
index 19c3720..6d096a1 100644
--- a/compos/composd_cmd/composd_cmd.rs
+++ b/compos/composd_cmd/composd_cmd.rs
@@ -128,6 +128,12 @@
         &Strong<dyn ICompilationTaskCallback>,
     ) -> BinderResult<Strong<dyn ICompilationTask>>,
 {
+    if !hypervisor_props::is_any_vm_supported()? {
+        // Give up now, before trying to start composd, or we may end up waiting forever
+        // as it repeatedly starts and then aborts (b/254599807).
+        bail!("Device doesn't support protected or non-protected VMs")
+    }
+
     let service = wait_for_interface::<dyn IIsolatedCompilationService>("android.system.composd")
         .context("Failed to connect to composd service")?;
 
diff --git a/compos/verify/verify.rs b/compos/verify/verify.rs
index 71d8bcc..13e9292 100644
--- a/compos/verify/verify.rs
+++ b/compos/verify/verify.rs
@@ -21,7 +21,7 @@
 use anyhow::{bail, Context, Result};
 use binder::ProcessState;
 use clap::{Parser, ValueEnum};
-use compos_common::compos_client::{ComposClient, VmParameters};
+use compos_common::compos_client::{ComposClient, VmCpuTopology, VmParameters};
 use compos_common::odrefresh::{
     CURRENT_ARTIFACTS_SUBDIR, ODREFRESH_OUTPUT_ROOT_DIR, PENDING_ARTIFACTS_SUBDIR,
     TEST_ARTIFACTS_SUBDIR,
@@ -114,7 +114,11 @@
         &idsig,
         &idsig_manifest_apk,
         &idsig_manifest_ext_apk,
-        &VmParameters { debug_mode: args.debug, ..Default::default() },
+        &VmParameters {
+            cpu_topology: VmCpuTopology::OneCpu, // This VM runs very little work at boot
+            debug_mode: args.debug,
+            ..Default::default()
+        },
     )?;
 
     let service = vm_instance.connect_service()?;
diff --git a/docs/debug/gdb.md b/docs/debug/gdb.md
new file mode 100644
index 0000000..316faad
--- /dev/null
+++ b/docs/debug/gdb.md
@@ -0,0 +1,46 @@
+# Debugging guest kernels with gdb
+
+Note: this feature is only available on android14-5.15 and newer host kernels.
+
+Starting with Android U it is possible to attach a gdb to the guest kernel, when
+starting a debuggable and non-protected guest VM.
+
+You can do this by passing `--gdb <port>` argument to the `vm run`, `vm run-app`
+and `vm run-microdroid` commands. The `crosvm` will start the gdb server on the
+provided port. It will wait for the gdb client to connect to it before
+proceeding with the VM boot.
+
+Here is an example invocation:
+
+```shell
+adb forward tcp:3456 tcp:3456
+adb shell /apex/com.android.virt/bin/vm run-microdroid --gdb 3456
+```
+
+Then in another shell:
+
+```shell
+gdb vmlinux
+(gdb) target remote :3456
+(gdb) hbreak start_kernel
+(gdb) c
+```
+
+The [kernel documentation](
+https://www.kernel.org/doc/html/latest/dev-tools/gdb-kernel-debugging.html) has
+some general techniques on how to debug kernel with gdb.
+
+## Obtaining vmlinux for Microdroid kernels
+
+If you are debugging Microdroid kernel that you have built [locally](
+../../microdroid/kernel/README.md), then look for `out/dist/vmlinux` in your
+kernel repository.
+
+If you are debugging Microdroid kernel bundled with the `com.android.virt` APEX,
+then you need to obtain the build ID of this kernel. You can do this by
+checking the prebuilt-info.txt file in the
+`packages/modules/Virtualization/microdroid/kernel/arm64` or
+`packages/modules/Virtualization/microdroid/kernel/x86_64` directories.
+
+Using that build ID you can download the vmlinux from the build server via:
+https://ci.android.com/builds/submitted/${BUILD_ID}/kernel_microdroid_aarch64/latest/vmlinux
diff --git a/docs/debug/ramdump.md b/docs/debug/ramdump.md
index 771c608..020f054 100644
--- a/docs/debug/ramdump.md
+++ b/docs/debug/ramdump.md
@@ -73,8 +73,8 @@
 Download the source code and build it as follows. This needs to be done only once.
 
 ```shell
-$ wget https://github.com/crash-utility/crash/archive/refs/tags/8.0.1.tar.gz -O - | tar xzvf
-$ make -C crash-8.0.1 target=ARM64
+$ wget https://github.com/crash-utility/crash/archive/refs/tags/8.0.2.tar.gz -O - | tar xzv
+$ make -j -C crash-8.0.2 target=ARM64
 ```
 
 ### Obtaining vmlinux
@@ -101,7 +101,7 @@
 ### Running crash(8) with the RAM dump and the kernel image
 
 ```shell
-$ crash-8.0.1/crash ramdump vmlinux
+$ crash-8.0.2/crash ramdump vmlinux
 ```
 
 You can now analyze the RAM dump using the various commands that crash(8) provides. For example, `bt <pid>` command shows the stack trace of a process.
diff --git a/docs/debug/tracing.md b/docs/debug/tracing.md
new file mode 100644
index 0000000..7d7ea0c
--- /dev/null
+++ b/docs/debug/tracing.md
@@ -0,0 +1,75 @@
+# Hypervisor & guest tracing
+
+## Hypervisor tracing
+
+Starting with android14-5.15 kernel it is possible to get traces from the hypervisor.
+
+### User space interface
+
+The user space hypervisor tracing interface is located either at /sys/kernel/tracing/hyp or at
+/sys/kernel/debug/tracing/hyp. On the Android phones it will usually be /sys/kernel/tracing/hyp,
+while on QEMU it will be /sys/kernel/debug/tracing/hyp.
+
+The user space interface is very similar to the ftrace user space interface, however there are some
+differences, e.g.:
+
+* Only boot clock is supported, and there is no way for user space to change the tracing_clock.
+* Hypervisor tracing periodically polls the data from the hypervisor, this is different from the
+  regular ftrace instance which pushes the events into the ring buffer.
+
+Note: the list above is not exhaustive.
+
+TODO(b/271412868): add more documentation on the user space interface.
+
+### Perfetto integration
+
+[Perfetto](https://perfetto.dev/docs/) is an open-source stack for performance instrumentation and
+trace analysis widely used in  Android. Perfetto supports capturing and visualizing hypervisor
+traces.
+
+#### Capturing hypervisor traces on Android
+
+Consider first familiarizing yourself with Perfetto documentation for recording traces on Android:
+https://perfetto.dev/docs/quickstart/android-tracing.
+
+So far it is only possible to capture hypervisor trace events by providing the full trace config
+file to Perfetto. Here is the minimal
+
+```shell
+cat<<EOF>config.pbtx
+duration_ms: 10000
+
+buffers: {
+    size_kb: 8960
+    fill_policy: DISCARD
+}
+
+data_sources: {
+    config {
+        name: "linux.ftrace"
+        ftrace_config {
+            instance_name: "hyp"
+            ftrace_events: "hyp/hyp_enter"
+            ftrace_events: "hyp/hyp_exit"
+        }
+    }
+}
+EOF
+
+./record_android_trace -c config.pbtx -o trace_file.perfetto-trace
+```
+
+If you have an Android tree checked out, then record_android_trace helper script can be located at
+${REPO_ROOT}/external/perfetto/tools/record_android_traces. Otherwise, you can download the script
+by following steps outlined in the [Perfetto docs](
+https://perfetto.dev/docs/quickstart/android-tracing#recording-a-trace-through-the-cmdline)
+
+#### Capturing hypervisor traces on QEMU
+
+TODO(b/271412868): fill in this section
+
+TODO(b/271412868): Stay tuned, more docs coming soon!
+
+## Microdroid VM tracing
+
+TODO(b/271412868): Stay tuned, more docs are coming soon!
diff --git a/docs/getting_started/index.md b/docs/getting_started/index.md
index f184862..25eb909 100644
--- a/docs/getting_started/index.md
+++ b/docs/getting_started/index.md
@@ -97,15 +97,6 @@
 If you run into problems, inspect the logs produced by `atest`. Their location is printed at the
 end. The `host_log_*.zip` file should contain the output of individual commands as well as VM logs.
 
-### Custom pvmfw
-
-Hostside tests, which run on the PC and extends `MicrodroidHostTestCaseBase`, can be run with
-a custom `pvmfw`. Use `--module-arg` to push `pvmfw` for individual test methods.
-
-```shell
-atest com.android.microdroid.test.MicrodroidHostTests -- --module-arg MicrodroidHostTestCases:set-option:pvmfw:pvmfw.img
-```
-
 ## Spawning your own VMs with custom kernel
 
 You can spawn your own VMs by passing a JSON config file to the VirtualizationService via the `vm`
diff --git a/encryptedstore/src/main.rs b/encryptedstore/src/main.rs
index 2f54534..7a41f13 100644
--- a/encryptedstore/src/main.rs
+++ b/encryptedstore/src/main.rs
@@ -46,6 +46,7 @@
     let blkdevice = Path::new(matches.get_one::<String>("blkdevice").unwrap());
     let key = matches.get_one::<String>("key").unwrap();
     let mountpoint = Path::new(matches.get_one::<String>("mountpoint").unwrap());
+    // Note this error context is used in MicrodroidTests.
     encryptedstore_init(blkdevice, key, mountpoint).context(format!(
         "Unable to initialize encryptedstore on {:?} & mount at {:?}",
         blkdevice, mountpoint
@@ -94,6 +95,8 @@
         .data_device(data_device, dev_size)
         .cipher(CipherType::AES256HCTR2)
         .key(&key)
+        .opt_param("sector_size:4096")
+        .opt_param("iv_large_sectors")
         .build()
         .context("Couldn't build the DMCrypt target")?;
     let dm = dm::DeviceMapper::new()?;
@@ -124,6 +127,7 @@
     let mkfs_options = [
         "-j",               // Create appropriate sized journal
         "-O metadata_csum", // Metadata checksum for filesystem integrity
+        "-b 4096",          // block size in the filesystem
     ];
     let mut cmd = Command::new(MK2FS_BIN);
     let status = cmd
diff --git a/javalib/api/system-current.txt b/javalib/api/system-current.txt
index b455c85..d9bafa1 100644
--- a/javalib/api/system-current.txt
+++ b/javalib/api/system-current.txt
@@ -57,15 +57,17 @@
 
   public final class VirtualMachineConfig {
     method @Nullable public String getApkPath();
-    method @NonNull public int getDebugLevel();
+    method public int getCpuTopology();
+    method public int getDebugLevel();
     method @IntRange(from=0) public long getEncryptedStorageBytes();
     method @IntRange(from=0) public long getMemoryBytes();
-    method @IntRange(from=1) public int getNumCpus();
     method @Nullable public String getPayloadBinaryName();
     method public boolean isCompatibleWith(@NonNull android.system.virtualmachine.VirtualMachineConfig);
     method public boolean isEncryptedStorageEnabled();
     method public boolean isProtectedVm();
     method public boolean isVmOutputCaptured();
+    field public static final int CPU_TOPOLOGY_MATCH_HOST = 1; // 0x1
+    field public static final int CPU_TOPOLOGY_ONE_CPU = 0; // 0x0
     field public static final int DEBUG_LEVEL_FULL = 1; // 0x1
     field public static final int DEBUG_LEVEL_NONE = 0; // 0x0
   }
@@ -74,16 +76,17 @@
     ctor public VirtualMachineConfig.Builder(@NonNull android.content.Context);
     method @NonNull public android.system.virtualmachine.VirtualMachineConfig build();
     method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setApkPath(@NonNull String);
+    method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setCpuTopology(int);
     method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setDebugLevel(int);
     method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setEncryptedStorageBytes(@IntRange(from=1) long);
     method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setMemoryBytes(@IntRange(from=1) long);
-    method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setNumCpus(@IntRange(from=1) int);
     method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setPayloadBinaryName(@NonNull String);
     method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setProtectedVm(boolean);
     method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setVmOutputCaptured(boolean);
   }
 
-  public final class VirtualMachineDescriptor implements android.os.Parcelable {
+  public final class VirtualMachineDescriptor implements java.lang.AutoCloseable android.os.Parcelable {
+    method public void close();
     method public int describeContents();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.system.virtualmachine.VirtualMachineDescriptor> CREATOR;
diff --git a/javalib/jni/android_system_virtualmachine_VirtualMachine.cpp b/javalib/jni/android_system_virtualmachine_VirtualMachine.cpp
index afdc944..b3354cc 100644
--- a/javalib/jni/android_system_virtualmachine_VirtualMachine.cpp
+++ b/javalib/jni/android_system_virtualmachine_VirtualMachine.cpp
@@ -57,6 +57,11 @@
     };
 
     RpcSessionHandle session;
+    // We need a thread pool to be able to support linkToDeath, or callbacks
+    // (b/268335700). These threads are currently created eagerly, so we don't
+    // want too many. The number 1 is chosen after some discussion, and to match
+    // the server-side default (mMaxThreads on RpcServer).
+    ARpcSession_setMaxIncomingThreads(session.get(), 1);
     auto client = ARpcSession_setupPreconnectedClient(session.get(), requestFunc, &args);
     return AIBinder_toJavaBinder(env, client);
 }
diff --git a/javalib/jni/android_system_virtualmachine_VirtualizationService.cpp b/javalib/jni/android_system_virtualmachine_VirtualizationService.cpp
index bd80880..fbd1fd5 100644
--- a/javalib/jni/android_system_virtualmachine_VirtualizationService.cpp
+++ b/javalib/jni/android_system_virtualmachine_VirtualizationService.cpp
@@ -29,7 +29,7 @@
 using namespace android::base;
 
 static constexpr const char VIRTMGR_PATH[] = "/apex/com.android.virt/bin/virtmgr";
-static constexpr size_t VIRTMGR_THREADS = 16;
+static constexpr size_t VIRTMGR_THREADS = 2;
 
 extern "C" JNIEXPORT jint JNICALL
 Java_android_system_virtualmachine_VirtualizationService_nativeSpawn(
@@ -83,7 +83,6 @@
     ARpcSession_setFileDescriptorTransportMode(session.get(),
                                                ARpcSession_FileDescriptorTransportMode::Unix);
     ARpcSession_setMaxIncomingThreads(session.get(), VIRTMGR_THREADS);
-    ARpcSession_setMaxOutgoingThreads(session.get(), VIRTMGR_THREADS);
     // SAFETY - ARpcSession_setupUnixDomainBootstrapClient does not take ownership of clientFd.
     auto client = ARpcSession_setupUnixDomainBootstrapClient(session.get(), clientFd);
     return AIBinder_toJavaBinder(env, client);
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachine.java b/javalib/src/android/system/virtualmachine/VirtualMachine.java
index ba7174e..5f39b1c 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachine.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachine.java
@@ -377,7 +377,7 @@
     }
 
     /**
-     * Builds a virtual machine from an {@link VirtualMachineDescriptor} object and associates it
+     * Creates a virtual machine from an {@link VirtualMachineDescriptor} object and associates it
      * with the given name.
      *
      * <p>The new virtual machine will be in the same state as the descriptor indicates.
@@ -393,27 +393,29 @@
             @NonNull String name,
             @NonNull VirtualMachineDescriptor vmDescriptor)
             throws VirtualMachineException {
-        VirtualMachineConfig config = VirtualMachineConfig.from(vmDescriptor.getConfigFd());
         File vmDir = createVmDir(context, name);
         try {
-            VirtualMachine vm =
-                    new VirtualMachine(context, name, config, VirtualizationService.getInstance());
-            config.serialize(vm.mConfigFilePath);
-            try {
-                vm.mInstanceFilePath.createNewFile();
-            } catch (IOException e) {
-                throw new VirtualMachineException("failed to create instance image", e);
-            }
-            vm.importInstanceFrom(vmDescriptor.getInstanceImgFd());
-
-            if (vmDescriptor.getEncryptedStoreFd() != null) {
+            VirtualMachine vm;
+            try (vmDescriptor) {
+                VirtualMachineConfig config = VirtualMachineConfig.from(vmDescriptor.getConfigFd());
+                vm = new VirtualMachine(context, name, config, VirtualizationService.getInstance());
+                config.serialize(vm.mConfigFilePath);
                 try {
-                    vm.mEncryptedStoreFilePath.createNewFile();
+                    vm.mInstanceFilePath.createNewFile();
                 } catch (IOException e) {
-                    throw new VirtualMachineException(
-                            "failed to create encrypted storage image", e);
+                    throw new VirtualMachineException("failed to create instance image", e);
                 }
-                vm.importEncryptedStoreFrom(vmDescriptor.getEncryptedStoreFd());
+                vm.importInstanceFrom(vmDescriptor.getInstanceImgFd());
+
+                if (vmDescriptor.getEncryptedStoreFd() != null) {
+                    try {
+                        vm.mEncryptedStoreFilePath.createNewFile();
+                    } catch (IOException e) {
+                        throw new VirtualMachineException(
+                                "failed to create encrypted storage image", e);
+                    }
+                    vm.importEncryptedStoreFrom(vmDescriptor.getEncryptedStoreFd());
+                }
             }
             return vm;
         } catch (VirtualMachineException | RuntimeException e) {
@@ -1031,11 +1033,13 @@
             }
             checkStopped();
 
-            // Delete any existing file before recreating; that ensures any VirtualMachineDescriptor
-            // that refers to the old file does not see the new config.
-            mConfigFilePath.delete();
-            newConfig.serialize(mConfigFilePath);
-            mConfig = newConfig;
+            if (oldConfig != newConfig) {
+                // Delete any existing file before recreating; that ensures any
+                // VirtualMachineDescriptor that refers to the old file does not see the new config.
+                mConfigFilePath.delete();
+                newConfig.serialize(mConfigFilePath);
+                mConfig = newConfig;
+            }
             return oldConfig;
         }
     }
@@ -1227,8 +1231,7 @@
         if (configPath == null) {
             return Collections.emptyList();
         }
-        try {
-            ZipFile zipFile = new ZipFile(context.getPackageCodePath());
+        try (ZipFile zipFile = new ZipFile(context.getPackageCodePath())) {
             InputStream inputStream =
                     zipFile.getInputStream(zipFile.getEntry(configPath));
             List<String> apkList =
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
index cb9bad0..93e65db 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -31,11 +31,13 @@
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
+import android.os.Build;
 import android.os.ParcelFileDescriptor;
 import android.os.PersistableBundle;
 import android.sysprop.HypervisorProperties;
 import android.system.virtualizationservice.VirtualMachineAppConfig;
 import android.system.virtualizationservice.VirtualMachinePayloadConfig;
+import android.util.Log;
 
 import java.io.File;
 import java.io.FileInputStream;
@@ -47,6 +49,7 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.Objects;
+import java.util.zip.ZipFile;
 
 /**
  * Represents a configuration of a virtual machine. A configuration consists of hardware
@@ -57,10 +60,11 @@
  */
 @SystemApi
 public final class VirtualMachineConfig {
+    private static final String TAG = "VirtualMachineConfig";
     private static final String[] EMPTY_STRING_ARRAY = {};
 
     // These define the schema of the config file persisted on disk.
-    private static final int VERSION = 5;
+    private static final int VERSION = 6;
     private static final String KEY_VERSION = "version";
     private static final String KEY_PACKAGENAME = "packageName";
     private static final String KEY_APKPATH = "apkPath";
@@ -69,7 +73,7 @@
     private static final String KEY_DEBUGLEVEL = "debugLevel";
     private static final String KEY_PROTECTED_VM = "protectedVm";
     private static final String KEY_MEMORY_BYTES = "memoryBytes";
-    private static final String KEY_NUM_CPUS = "numCpus";
+    private static final String KEY_CPU_TOPOLOGY = "cpuTopology";
     private static final String KEY_ENCRYPTED_STORAGE_BYTES = "encryptedStorageBytes";
     private static final String KEY_VM_OUTPUT_CAPTURED = "vmOutputCaptured";
 
@@ -97,6 +101,33 @@
      */
     @SystemApi public static final int DEBUG_LEVEL_FULL = 1;
 
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(
+            prefix = "CPU_TOPOLOGY_",
+            value = {
+                CPU_TOPOLOGY_ONE_CPU,
+                CPU_TOPOLOGY_MATCH_HOST,
+            })
+    public @interface CpuTopology {}
+
+    /**
+     * Run VM with 1 vCPU. This is the default option, usually the fastest to boot and consuming the
+     * least amount of resources. Typically the best option for small or ephemeral workloads.
+     *
+     * @hide
+     */
+    @SystemApi public static final int CPU_TOPOLOGY_ONE_CPU = 0;
+
+    /**
+     * Run VM with vCPU topology matching the physical CPU topology of the host. Usually takes
+     * longer to boot and cosumes more resources compared to a single vCPU. Typically a good option
+     * for long-running workloads that benefit from parallel execution.
+     *
+     * @hide
+     */
+    @SystemApi public static final int CPU_TOPOLOGY_MATCH_HOST = 1;
+
     /** Name of a package whose primary APK contains the VM payload. */
     @Nullable private final String mPackageName;
 
@@ -116,10 +147,8 @@
      */
     private final long mMemoryBytes;
 
-    /**
-     * Number of vCPUs in the VM. Defaults to 1 when not specified.
-     */
-    private final int mNumCpus;
+    /** CPU topology configuration of the VM. */
+    @CpuTopology private final int mCpuTopology;
 
     /**
      * Path within the APK to the payload config file that defines software aspects of the VM.
@@ -143,7 +172,7 @@
             @DebugLevel int debugLevel,
             boolean protectedVm,
             long memoryBytes,
-            int numCpus,
+            @CpuTopology int cpuTopology,
             long encryptedStorageBytes,
             boolean vmOutputCaptured) {
         // This is only called from Builder.build(); the builder handles parameter validation.
@@ -154,7 +183,7 @@
         mDebugLevel = debugLevel;
         mProtectedVm = protectedVm;
         mMemoryBytes = memoryBytes;
-        mNumCpus = numCpus;
+        mCpuTopology = cpuTopology;
         mEncryptedStorageBytes = encryptedStorageBytes;
         mVmOutputCaptured = vmOutputCaptured;
     }
@@ -225,7 +254,7 @@
         if (memoryBytes != 0) {
             builder.setMemoryBytes(memoryBytes);
         }
-        builder.setNumCpus(b.getInt(KEY_NUM_CPUS));
+        builder.setCpuTopology(b.getInt(KEY_CPU_TOPOLOGY));
         long encryptedStorageBytes = b.getLong(KEY_ENCRYPTED_STORAGE_BYTES);
         if (encryptedStorageBytes != 0) {
             builder.setEncryptedStorageBytes(encryptedStorageBytes);
@@ -258,7 +287,7 @@
         b.putString(KEY_PAYLOADBINARYNAME, mPayloadBinaryName);
         b.putInt(KEY_DEBUGLEVEL, mDebugLevel);
         b.putBoolean(KEY_PROTECTED_VM, mProtectedVm);
-        b.putInt(KEY_NUM_CPUS, mNumCpus);
+        b.putInt(KEY_CPU_TOPOLOGY, mCpuTopology);
         if (mMemoryBytes > 0) {
             b.putLong(KEY_MEMORY_BYTES, mMemoryBytes);
         }
@@ -271,8 +300,7 @@
 
     /**
      * Returns the absolute path of the APK which should contain the binary payload that will
-     * execute within the VM. Returns null if no specific path has been set, so the primary APK will
-     * be used.
+     * execute within the VM. Returns null if no specific path has been set.
      *
      * @hide
      */
@@ -312,7 +340,6 @@
      * @hide
      */
     @SystemApi
-    @NonNull
     @DebugLevel
     public int getDebugLevel() {
         return mDebugLevel;
@@ -341,14 +368,14 @@
     }
 
     /**
-     * Returns the number of vCPUs that the VM will have.
+     * Returns the CPU topology configuration of the VM.
      *
      * @hide
      */
     @SystemApi
-    @IntRange(from = 1)
-    public int getNumCpus() {
-        return mNumCpus;
+    @CpuTopology
+    public int getCpuTopology() {
+        return mCpuTopology;
     }
 
     /**
@@ -397,6 +424,9 @@
      */
     @SystemApi
     public boolean isCompatibleWith(@NonNull VirtualMachineConfig other) {
+        if (this == other) {
+            return true;
+        }
         return this.mDebugLevel == other.mDebugLevel
                 && this.mProtectedVm == other.mProtectedVm
                 && this.mEncryptedStorageBytes == other.mEncryptedStorageBytes
@@ -418,18 +448,7 @@
             throws VirtualMachineException {
         VirtualMachineAppConfig vsConfig = new VirtualMachineAppConfig();
 
-        String apkPath = mApkPath;
-        if (apkPath == null) {
-            try {
-                ApplicationInfo appInfo =
-                        packageManager.getApplicationInfo(
-                                mPackageName, PackageManager.ApplicationInfoFlags.of(0));
-                // This really is the path to the APK, not a directory.
-                apkPath = appInfo.sourceDir;
-            } catch (PackageManager.NameNotFoundException e) {
-                throw new VirtualMachineException("Package not found", e);
-            }
-        }
+        String apkPath = (mApkPath != null) ? mApkPath : findPayloadApk(packageManager);
 
         try {
             vsConfig.apk = ParcelFileDescriptor.open(new File(apkPath), MODE_READ_ONLY);
@@ -455,12 +474,58 @@
         }
         vsConfig.protectedVm = mProtectedVm;
         vsConfig.memoryMib = bytesToMebiBytes(mMemoryBytes);
-        vsConfig.numCpus = mNumCpus;
+        switch (mCpuTopology) {
+            case CPU_TOPOLOGY_MATCH_HOST:
+                vsConfig.cpuTopology = android.system.virtualizationservice.CpuTopology.MATCH_HOST;
+                break;
+            default:
+                vsConfig.cpuTopology = android.system.virtualizationservice.CpuTopology.ONE_CPU;
+                break;
+        }
         // Don't allow apps to set task profiles ... at least for now.
         vsConfig.taskProfiles = EMPTY_STRING_ARRAY;
         return vsConfig;
     }
 
+    private String findPayloadApk(PackageManager packageManager) throws VirtualMachineException {
+        ApplicationInfo appInfo;
+        try {
+            appInfo =
+                    packageManager.getApplicationInfo(
+                            mPackageName, PackageManager.ApplicationInfoFlags.of(0));
+        } catch (PackageManager.NameNotFoundException e) {
+            throw new VirtualMachineException("Package not found", e);
+        }
+
+        String[] splitApkPaths = appInfo.splitSourceDirs;
+        String[] abis = Build.SUPPORTED_64_BIT_ABIS;
+
+        // If there are split APKs, and we know the payload binary name, see if we can find a
+        // split APK containing the binary.
+        if (mPayloadBinaryName != null && splitApkPaths != null && abis.length != 0) {
+            String[] libraryNames = new String[abis.length];
+            for (int i = 0; i < abis.length; i++) {
+                libraryNames[i] = "lib/" + abis[i] + "/" + mPayloadBinaryName;
+            }
+
+            for (String path : splitApkPaths) {
+                try (ZipFile zip = new ZipFile(path)) {
+                    for (String name : libraryNames) {
+                        if (zip.getEntry(name) != null) {
+                            Log.i(TAG, "Found payload in " + path);
+                            return path;
+                        }
+                    }
+                } catch (IOException e) {
+                    Log.w(TAG, "Failed to scan split APK: " + path, e);
+                }
+            }
+        }
+
+        // This really is the path to the APK, not a directory.
+        return appInfo.sourceDir;
+    }
+
     private int bytesToMebiBytes(long mMemoryBytes) {
         long oneMebi = 1024 * 1024;
         // We can't express requests for more than 2 exabytes, but then they're not going to succeed
@@ -486,7 +551,7 @@
         private boolean mProtectedVm;
         private boolean mProtectedVmSet;
         private long mMemoryBytes;
-        private int mNumCpus = 1;
+        @CpuTopology private int mCpuTopology = CPU_TOPOLOGY_ONE_CPU;
         private long mEncryptedStorageBytes;
         private boolean mVmOutputCaptured = false;
 
@@ -555,14 +620,15 @@
                     mDebugLevel,
                     mProtectedVm,
                     mMemoryBytes,
-                    mNumCpus,
+                    mCpuTopology,
                     mEncryptedStorageBytes,
                     mVmOutputCaptured);
         }
 
         /**
          * Sets the absolute path of the APK containing the binary payload that will execute within
-         * the VM. If not set explicitly, defaults to the primary APK of the context.
+         * the VM. If not set explicitly, defaults to the split APK containing the payload, if there
+         * is one, and otherwise the primary APK of the context.
          *
          * @hide
          */
@@ -687,25 +753,21 @@
         }
 
         /**
-         * Sets the number of vCPUs in the VM. Defaults to 1. Cannot be more than the number of real
-         * CPUs (as returned by {@link Runtime#availableProcessors}).
+         * Sets the CPU topology configuration of the VM. Defaults to {@link #CPU_TOPOLOGY_ONE_CPU}.
+         *
+         * <p>This determines how many virtual CPUs will be created, and their performance and
+         * scheduling characteristics, such as affinity masks. Topology also has an effect on memory
+         * usage as each vCPU requires additional memory to keep its state.
          *
          * @hide
          */
         @SystemApi
         @NonNull
-        public Builder setNumCpus(@IntRange(from = 1) int numCpus) {
-            int availableCpus = Runtime.getRuntime().availableProcessors();
-            if (numCpus < 1 || numCpus > availableCpus) {
-                throw new IllegalArgumentException(
-                        "Number of vCPUs ("
-                                + numCpus
-                                + ") is out of "
-                                + "range [1, "
-                                + availableCpus
-                                + "]");
+        public Builder setCpuTopology(@CpuTopology int cpuTopology) {
+            if (cpuTopology != CPU_TOPOLOGY_ONE_CPU && cpuTopology != CPU_TOPOLOGY_MATCH_HOST) {
+                throw new IllegalArgumentException("Invalid cpuTopology: " + cpuTopology);
             }
-            mNumCpus = numCpus;
+            mCpuTopology = cpuTopology;
             return this;
         }
 
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineDescriptor.java b/javalib/src/android/system/virtualmachine/VirtualMachineDescriptor.java
index 483779a..710925d 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineDescriptor.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineDescriptor.java
@@ -25,6 +25,8 @@
 import android.os.ParcelFileDescriptor;
 import android.os.Parcelable;
 
+import java.io.IOException;
+
 /**
  * A VM descriptor that captures the state of a Virtual Machine.
  *
@@ -35,7 +37,8 @@
  * @hide
  */
 @SystemApi
-public final class VirtualMachineDescriptor implements Parcelable {
+public final class VirtualMachineDescriptor implements Parcelable, AutoCloseable {
+    private volatile boolean mClosed = false;
     @NonNull private final ParcelFileDescriptor mConfigFd;
     @NonNull private final ParcelFileDescriptor mInstanceImgFd;
     // File descriptor of the image backing the encrypted storage - Will be null if encrypted
@@ -49,9 +52,10 @@
 
     @Override
     public void writeToParcel(@NonNull Parcel out, int flags) {
-        mConfigFd.writeToParcel(out, flags);
-        mInstanceImgFd.writeToParcel(out, flags);
-        if (mEncryptedStoreFd != null) mEncryptedStoreFd.writeToParcel(out, flags);
+        checkNotClosed();
+        out.writeParcelable(mConfigFd, flags);
+        out.writeParcelable(mInstanceImgFd, flags);
+        out.writeParcelable(mEncryptedStoreFd, flags);
     }
 
     @NonNull
@@ -71,6 +75,7 @@
      */
     @NonNull
     ParcelFileDescriptor getConfigFd() {
+        checkNotClosed();
         return mConfigFd;
     }
 
@@ -79,6 +84,7 @@
      */
     @NonNull
     ParcelFileDescriptor getInstanceImgFd() {
+        checkNotClosed();
         return mInstanceImgFd;
     }
 
@@ -88,6 +94,7 @@
      */
     @Nullable
     ParcelFileDescriptor getEncryptedStoreFd() {
+        checkNotClosed();
         return mEncryptedStoreFd;
     }
 
@@ -95,14 +102,42 @@
             @NonNull ParcelFileDescriptor configFd,
             @NonNull ParcelFileDescriptor instanceImgFd,
             @Nullable ParcelFileDescriptor encryptedStoreFd) {
-        mConfigFd = configFd;
-        mInstanceImgFd = instanceImgFd;
+        mConfigFd = requireNonNull(configFd);
+        mInstanceImgFd = requireNonNull(instanceImgFd);
         mEncryptedStoreFd = encryptedStoreFd;
     }
 
     private VirtualMachineDescriptor(Parcel in) {
-        mConfigFd = requireNonNull(in.readFileDescriptor());
-        mInstanceImgFd = requireNonNull(in.readFileDescriptor());
-        mEncryptedStoreFd = in.readFileDescriptor();
+        mConfigFd = requireNonNull(readParcelFileDescriptor(in));
+        mInstanceImgFd = requireNonNull(readParcelFileDescriptor(in));
+        mEncryptedStoreFd = readParcelFileDescriptor(in);
+    }
+
+    private ParcelFileDescriptor readParcelFileDescriptor(Parcel in) {
+        return in.readParcelable(
+                ParcelFileDescriptor.class.getClassLoader(), ParcelFileDescriptor.class);
+    }
+
+    /**
+     * Release any resources held by this descriptor. Calling {@code close} on an already-closed
+     * descriptor has no effect.
+     */
+    @Override
+    public void close() {
+        mClosed = true;
+        // Let the compiler do the work: close everything, throw if any of them fail, skipping null.
+        try (mConfigFd;
+                mInstanceImgFd;
+                mEncryptedStoreFd) {
+        } catch (IOException ignored) {
+            // PFD already swallows exceptions from closing the fd. There's no reason to propagate
+            // this to the caller.
+        }
+    }
+
+    private void checkNotClosed() {
+        if (mClosed) {
+            throw new IllegalStateException("Descriptor has been closed");
+        }
     }
 }
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineManager.java b/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
index 7773cb5..b7ea22c 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
@@ -191,11 +191,13 @@
      * Imports a virtual machine from an {@link VirtualMachineDescriptor} object and associates it
      * with the given name.
      *
-     * <p>The new virtual machine will be in the same state as the descriptor indicates.
+     * <p>The new virtual machine will be in the same state as the descriptor indicates. The
+     * descriptor is automatically closed and cannot be used again.
      *
      * <p>NOTE: This method may block and should not be called on the main thread.
      *
-     * @throws VirtualMachineException if the VM cannot be imported.
+     * @throws VirtualMachineException if the VM cannot be imported or the {@code
+     *     VirtualMachineDescriptor} has already been closed.
      * @hide
      */
     @NonNull
diff --git a/libs/apkverify/Android.bp b/libs/apkverify/Android.bp
index e556842..83dbff6 100644
--- a/libs/apkverify/Android.bp
+++ b/libs/apkverify/Android.bp
@@ -45,7 +45,9 @@
     edition: "2021",
     test_suites: ["general-tests"],
     rustlibs: [
+        "libandroid_logger",
         "libapkverify",
+        "liblog_rust",
         "libzip",
     ],
     data: ["tests/data/*"],
diff --git a/libs/apkverify/src/algorithms.rs b/libs/apkverify/src/algorithms.rs
index 442b47c..c05ab38 100644
--- a/libs/apkverify/src/algorithms.rs
+++ b/libs/apkverify/src/algorithms.rs
@@ -204,9 +204,10 @@
 }
 
 /// Hash algorithms.
-#[derive(Clone, Copy, Debug, PartialEq, Eq, FromPrimitive, ToPrimitive)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, FromPrimitive, ToPrimitive, Default)]
 #[repr(u32)]
 pub enum HashAlgorithm {
+    #[default]
     /// SHA-256
     SHA256 = 1,
 }
@@ -217,9 +218,3 @@
         Self::from_u32(val).context(format!("Unsupported hash algorithm: {}", val))
     }
 }
-
-impl Default for HashAlgorithm {
-    fn default() -> Self {
-        HashAlgorithm::SHA256
-    }
-}
diff --git a/libs/apkverify/src/v3.rs b/libs/apkverify/src/v3.rs
index e1b728d..6082422 100644
--- a/libs/apkverify/src/v3.rs
+++ b/libs/apkverify/src/v3.rs
@@ -24,7 +24,7 @@
 use openssl::x509::X509;
 use std::fs::File;
 use std::io::{Read, Seek};
-use std::ops::Range;
+use std::ops::RangeInclusive;
 use std::path::Path;
 
 use crate::algorithms::SignatureAlgorithmID;
@@ -33,11 +33,9 @@
 
 pub const APK_SIGNATURE_SCHEME_V3_BLOCK_ID: u32 = 0xf05368c0;
 
-// TODO(b/190343842): get "ro.build.version.sdk"
-const SDK_INT: u32 = 31;
-
 type Signers = LengthPrefixed<Vec<LengthPrefixed<Signer>>>;
 
+#[derive(Debug)]
 pub(crate) struct Signer {
     signed_data: LengthPrefixed<Bytes>, // not verified yet
     min_sdk: u32,
@@ -47,8 +45,8 @@
 }
 
 impl Signer {
-    fn sdk_range(&self) -> Range<u32> {
-        self.min_sdk..self.max_sdk
+    fn sdk_range(&self) -> RangeInclusive<u32> {
+        self.min_sdk..=self.max_sdk
     }
 }
 
@@ -62,8 +60,8 @@
 }
 
 impl SignedData {
-    fn sdk_range(&self) -> Range<u32> {
-        self.min_sdk..self.max_sdk
+    fn sdk_range(&self) -> RangeInclusive<u32> {
+        self.min_sdk..=self.max_sdk
     }
 
     fn find_digest_by_algorithm(&self, algorithm_id: SignatureAlgorithmID) -> Result<&Digest> {
@@ -92,32 +90,30 @@
 
 /// Verifies APK Signature Scheme v3 signatures of the provided APK and returns the public key
 /// associated with the signer in DER format.
-pub fn verify<P: AsRef<Path>>(apk_path: P) -> Result<Box<[u8]>> {
+pub fn verify<P: AsRef<Path>>(apk_path: P, current_sdk: u32) -> Result<Box<[u8]>> {
     let apk = File::open(apk_path.as_ref())?;
-    let (signer, mut sections) = extract_signer_and_apk_sections(apk)?;
+    let (signer, mut sections) = extract_signer_and_apk_sections(apk, current_sdk)?;
     signer.verify(&mut sections)
 }
 
 /// Gets the public key (in DER format) that was used to sign the given APK/APEX file
-pub fn get_public_key_der<P: AsRef<Path>>(apk_path: P) -> Result<Box<[u8]>> {
+pub fn get_public_key_der<P: AsRef<Path>>(apk_path: P, current_sdk: u32) -> Result<Box<[u8]>> {
     let apk = File::open(apk_path.as_ref())?;
-    let (signer, _) = extract_signer_and_apk_sections(apk)?;
+    let (signer, _) = extract_signer_and_apk_sections(apk, current_sdk)?;
     Ok(signer.public_key.public_key_to_der()?.into_boxed_slice())
 }
 
 pub(crate) fn extract_signer_and_apk_sections<R: Read + Seek>(
     apk: R,
+    current_sdk: u32,
 ) -> Result<(Signer, ApkSections<R>)> {
     let mut sections = ApkSections::new(apk)?;
     let mut block = sections.find_signature(APK_SIGNATURE_SCHEME_V3_BLOCK_ID).context(
         "Fallback to v2 when v3 block not found is not yet implemented.", // b/197052981
     )?;
-    let mut supported = block
-        .read::<Signers>()?
-        .into_inner()
-        .into_iter()
-        .filter(|s| s.sdk_range().contains(&SDK_INT))
-        .collect::<Vec<_>>();
+    let signers = block.read::<Signers>()?.into_inner();
+    let mut supported =
+        signers.into_iter().filter(|s| s.sdk_range().contains(&current_sdk)).collect::<Vec<_>>();
     ensure!(
         supported.len() == 1,
         "APK Signature Scheme V3 only supports one signer: {} signers found.",
diff --git a/libs/apkverify/src/v4.rs b/libs/apkverify/src/v4.rs
index 94abf99..045f4af 100644
--- a/libs/apkverify/src/v4.rs
+++ b/libs/apkverify/src/v4.rs
@@ -37,9 +37,10 @@
 /// [apk_digest]: https://source.android.com/docs/security/apksigning/v4#apk-digest
 pub fn get_apk_digest<R: Read + Seek>(
     apk: R,
+    current_sdk: u32,
     verify: bool,
 ) -> Result<(SignatureAlgorithmID, Box<[u8]>)> {
-    let (signer, mut sections) = extract_signer_and_apk_sections(apk)?;
+    let (signer, mut sections) = extract_signer_and_apk_sections(apk, current_sdk)?;
     let strongest_algorithm_id = signer
         .strongest_signature()?
         .signature_algorithm_id
@@ -104,9 +105,10 @@
 }
 
 /// Version of the idsig file format
-#[derive(Debug, PartialEq, Eq, FromPrimitive, ToPrimitive)]
+#[derive(Debug, PartialEq, Eq, FromPrimitive, ToPrimitive, Default)]
 #[repr(u32)]
 pub enum Version {
+    #[default]
     /// Version 2, the only supported version.
     V2 = 2,
 }
@@ -117,12 +119,6 @@
     }
 }
 
-impl Default for Version {
-    fn default() -> Self {
-        Version::V2
-    }
-}
-
 impl V4Signature<fs::File> {
     /// Creates a `V4Signature` struct from the given idsig path.
     pub fn from_idsig_path<P: AsRef<Path>>(idsig_path: P) -> Result<Self> {
@@ -153,6 +149,7 @@
     /// function OOMing.
     pub fn create(
         mut apk: &mut R,
+        current_sdk: u32,
         block_size: usize,
         salt: &[u8],
         algorithm: HashAlgorithm,
@@ -180,7 +177,8 @@
         ret.hashing_info.log2_blocksize = log2(block_size);
 
         apk.seek(SeekFrom::Start(start))?;
-        let (signature_algorithm_id, apk_digest) = get_apk_digest(apk, /*verify=*/ false)?;
+        let (signature_algorithm_id, apk_digest) =
+            get_apk_digest(apk, current_sdk, /*verify=*/ false)?;
         ret.signing_info.signature_algorithm_id = signature_algorithm_id;
         ret.signing_info.apk_digest = apk_digest;
         // TODO(jiyong): add a signature to the signing_info struct
@@ -367,8 +365,9 @@
     #[test]
     fn digest_from_apk() {
         let mut input = Cursor::new(include_bytes!("../tests/data/v4-digest-v3-Sha256withEC.apk"));
+        let current_sdk = 31;
         let mut created =
-            V4Signature::create(&mut input, 4096, &[], HashAlgorithm::SHA256).unwrap();
+            V4Signature::create(&mut input, current_sdk, 4096, &[], HashAlgorithm::SHA256).unwrap();
 
         let mut golden = V4Signature::from_idsig_path(format!("{}.idsig", TEST_APK_PATH)).unwrap();
 
diff --git a/libs/apkverify/src/ziputil.rs b/libs/apkverify/src/ziputil.rs
index cc8bc58..5e513a7 100644
--- a/libs/apkverify/src/ziputil.rs
+++ b/libs/apkverify/src/ziputil.rs
@@ -18,9 +18,12 @@
 
 use anyhow::{ensure, Result};
 use bytes::{Buf, BufMut};
-use std::io::{Read, Seek, SeekFrom};
+use std::io::{Read, Seek};
 use zip::ZipArchive;
 
+#[cfg(test)]
+use std::io::SeekFrom;
+
 const EOCD_SIZE_WITHOUT_COMMENT: usize = 22;
 const EOCD_CENTRAL_DIRECTORY_SIZE_FIELD_OFFSET: usize = 12;
 const EOCD_CENTRAL_DIRECTORY_OFFSET_FIELD_OFFSET: usize = 16;
@@ -45,7 +48,7 @@
     // retrieve reader back
     reader = archive.into_inner();
     // the current position should point EOCD offset
-    let eocd_offset = reader.seek(SeekFrom::Current(0))? as u32;
+    let eocd_offset = reader.stream_position()? as u32;
     let mut eocd = vec![0u8; eocd_size];
     reader.read_exact(&mut eocd)?;
     ensure!(
diff --git a/libs/apkverify/tests/apkverify_test.rs b/libs/apkverify/tests/apkverify_test.rs
index baf7c42..52e1da4 100644
--- a/libs/apkverify/tests/apkverify_test.rs
+++ b/libs/apkverify/tests/apkverify_test.rs
@@ -17,16 +17,30 @@
 use apkverify::{
     get_apk_digest, get_public_key_der, testing::assert_contains, verify, SignatureAlgorithmID,
 };
+use log::info;
 use std::{fs, matches, path::Path};
 
 const KEY_NAMES_DSA: &[&str] = &["1024", "2048", "3072"];
 const KEY_NAMES_ECDSA: &[&str] = &["p256", "p384", "p521"];
 const KEY_NAMES_RSA: &[&str] = &["1024", "2048", "3072", "4096", "8192", "16384"];
 
+const SDK_INT: u32 = 31;
+
+/// Make sure any logging from the code under test ends up in logcat.
+fn setup() {
+    android_logger::init_once(
+        android_logger::Config::default()
+            .with_tag("apkverify_test")
+            .with_min_level(log::Level::Info),
+    );
+    info!("Test starting");
+}
+
 #[test]
 fn test_verify_truncated_cd() {
+    setup();
     use zip::result::ZipError;
-    let res = verify("tests/data/v2-only-truncated-cd.apk");
+    let res = verify("tests/data/v2-only-truncated-cd.apk", SDK_INT);
     // TODO(b/190343842): consider making a helper for err assertion
     assert!(matches!(
         res.unwrap_err().root_cause().downcast_ref::<ZipError>().unwrap(),
@@ -36,13 +50,15 @@
 
 #[test]
 fn apex_signed_with_v3_rsa_pkcs1_sha512_is_valid() {
+    setup();
     validate_apk("tests/data/test.apex", SignatureAlgorithmID::RsaPkcs1V15WithSha512);
 }
 
 #[test]
 fn apks_signed_with_v3_dsa_sha256_are_not_supported() {
+    setup();
     for key_name in KEY_NAMES_DSA.iter() {
-        let res = verify(format!("tests/data/v3-only-with-dsa-sha256-{}.apk", key_name));
+        let res = verify(format!("tests/data/v3-only-with-dsa-sha256-{}.apk", key_name), SDK_INT);
         assert!(res.is_err(), "DSA algorithm is not supported for verification. See b/197052981.");
         assert_contains(&res.unwrap_err().to_string(), "No supported APK signatures found");
     }
@@ -50,6 +66,7 @@
 
 #[test]
 fn apks_signed_with_v3_ecdsa_sha256_are_valid() {
+    setup();
     for key_name in KEY_NAMES_ECDSA.iter() {
         validate_apk(
             format!("tests/data/v3-only-with-ecdsa-sha256-{}.apk", key_name),
@@ -60,6 +77,7 @@
 
 #[test]
 fn apks_signed_with_v3_ecdsa_sha512_are_valid() {
+    setup();
     for key_name in KEY_NAMES_ECDSA.iter() {
         validate_apk(
             format!("tests/data/v3-only-with-ecdsa-sha512-{}.apk", key_name),
@@ -70,6 +88,7 @@
 
 #[test]
 fn apks_signed_with_v3_rsa_pkcs1_sha256_are_valid() {
+    setup();
     for key_name in KEY_NAMES_RSA.iter() {
         validate_apk(
             format!("tests/data/v3-only-with-rsa-pkcs1-sha256-{}.apk", key_name),
@@ -80,6 +99,7 @@
 
 #[test]
 fn apks_signed_with_v3_rsa_pkcs1_sha512_are_valid() {
+    setup();
     for key_name in KEY_NAMES_RSA.iter() {
         validate_apk(
             format!("tests/data/v3-only-with-rsa-pkcs1-sha512-{}.apk", key_name),
@@ -89,13 +109,35 @@
 }
 
 #[test]
+fn test_verify_v3_sig_min_max_sdk() {
+    setup();
+    // The Signer for this APK has min_sdk=24, max_sdk=32.
+    let path = "tests/data/v31-rsa-2048_2-tgt-33-1-tgt-28.apk";
+
+    let res = verify(path, 23);
+    assert!(res.is_err());
+    assert_contains(&res.unwrap_err().to_string(), "0 signers found");
+
+    let res = verify(path, 24);
+    assert!(res.is_ok());
+
+    let res = verify(path, 32);
+    assert!(res.is_ok());
+
+    let res = verify(path, 33);
+    assert!(res.is_err());
+    assert_contains(&res.unwrap_err().to_string(), "0 signers found");
+}
+
+#[test]
 fn test_verify_v3_sig_does_not_verify() {
+    setup();
     let path_list = [
         "tests/data/v3-only-with-ecdsa-sha512-p521-sig-does-not-verify.apk",
         "tests/data/v3-only-with-rsa-pkcs1-sha256-3072-sig-does-not-verify.apk",
     ];
     for path in path_list.iter() {
-        let res = verify(path);
+        let res = verify(path, SDK_INT);
         assert!(res.is_err());
         assert_contains(&res.unwrap_err().to_string(), "Signature is invalid");
     }
@@ -103,22 +145,28 @@
 
 #[test]
 fn test_verify_v3_digest_mismatch() {
-    let res = verify("tests/data/v3-only-with-rsa-pkcs1-sha512-8192-digest-mismatch.apk");
+    setup();
+    let res = verify("tests/data/v3-only-with-rsa-pkcs1-sha512-8192-digest-mismatch.apk", SDK_INT);
     assert!(res.is_err());
     assert_contains(&res.unwrap_err().to_string(), "Digest mismatch");
 }
 
 #[test]
 fn test_verify_v3_wrong_apk_sig_block_magic() {
-    let res = verify("tests/data/v3-only-with-ecdsa-sha512-p384-wrong-apk-sig-block-magic.apk");
+    setup();
+    let res =
+        verify("tests/data/v3-only-with-ecdsa-sha512-p384-wrong-apk-sig-block-magic.apk", SDK_INT);
     assert!(res.is_err());
     assert_contains(&res.unwrap_err().to_string(), "No APK Signing Block");
 }
 
 #[test]
 fn test_verify_v3_apk_sig_block_size_mismatch() {
-    let res =
-        verify("tests/data/v3-only-with-rsa-pkcs1-sha512-4096-apk-sig-block-size-mismatch.apk");
+    setup();
+    let res = verify(
+        "tests/data/v3-only-with-rsa-pkcs1-sha512-4096-apk-sig-block-size-mismatch.apk",
+        SDK_INT,
+    );
     assert!(res.is_err());
     assert_contains(
         &res.unwrap_err().to_string(),
@@ -128,35 +176,40 @@
 
 #[test]
 fn test_verify_v3_cert_and_public_key_mismatch() {
-    let res = verify("tests/data/v3-only-cert-and-public-key-mismatch.apk");
+    setup();
+    let res = verify("tests/data/v3-only-cert-and-public-key-mismatch.apk", SDK_INT);
     assert!(res.is_err());
     assert_contains(&res.unwrap_err().to_string(), "Public key mismatch");
 }
 
 #[test]
 fn test_verify_v3_empty() {
-    let res = verify("tests/data/v3-only-empty.apk");
+    setup();
+    let res = verify("tests/data/v3-only-empty.apk", SDK_INT);
     assert!(res.is_err());
     assert_contains(&res.unwrap_err().to_string(), "APK too small for APK Signing Block");
 }
 
 #[test]
 fn test_verify_v3_no_certs_in_sig() {
-    let res = verify("tests/data/v3-only-no-certs-in-sig.apk");
+    setup();
+    let res = verify("tests/data/v3-only-no-certs-in-sig.apk", SDK_INT);
     assert!(res.is_err());
     assert_contains(&res.unwrap_err().to_string(), "No certificates listed");
 }
 
 #[test]
 fn test_verify_v3_no_supported_sig_algs() {
-    let res = verify("tests/data/v3-only-no-supported-sig-algs.apk");
+    setup();
+    let res = verify("tests/data/v3-only-no-supported-sig-algs.apk", SDK_INT);
     assert!(res.is_err());
     assert_contains(&res.unwrap_err().to_string(), "No supported APK signatures found");
 }
 
 #[test]
 fn test_verify_v3_signatures_and_digests_block_mismatch() {
-    let res = verify("tests/data/v3-only-signatures-and-digests-block-mismatch.apk");
+    setup();
+    let res = verify("tests/data/v3-only-signatures-and-digests-block-mismatch.apk", SDK_INT);
     assert!(res.is_err());
     assert_contains(
         &res.unwrap_err().to_string(),
@@ -166,6 +219,7 @@
 
 #[test]
 fn apk_signed_with_v3_unknown_additional_attr_is_valid() {
+    setup();
     validate_apk(
         "tests/data/v3-only-unknown-additional-attr.apk",
         SignatureAlgorithmID::RsaPkcs1V15WithSha256,
@@ -174,6 +228,7 @@
 
 #[test]
 fn apk_signed_with_v3_unknown_pair_in_apk_sig_block_is_valid() {
+    setup();
     validate_apk(
         "tests/data/v3-only-unknown-pair-in-apk-sig-block.apk",
         SignatureAlgorithmID::RsaPkcs1V15WithSha256,
@@ -182,6 +237,7 @@
 
 #[test]
 fn apk_signed_with_v3_ignorable_unsupported_sig_algs_is_valid() {
+    setup();
     validate_apk(
         "tests/data/v3-only-with-ignorable-unsupported-sig-algs.apk",
         SignatureAlgorithmID::RsaPkcs1V15WithSha256,
@@ -190,6 +246,7 @@
 
 #[test]
 fn apk_signed_with_v3_stamp_is_valid() {
+    setup();
     validate_apk("tests/data/v3-only-with-stamp.apk", SignatureAlgorithmID::EcdsaWithSha256);
 }
 
@@ -203,14 +260,14 @@
 /// * public key extracted from apk without verification
 /// * expected public key from the corresponding .der file
 fn validate_apk_public_key<P: AsRef<Path>>(apk_path: P) {
-    let public_key_from_verification = verify(&apk_path);
+    let public_key_from_verification = verify(&apk_path, SDK_INT);
     let public_key_from_verification =
         public_key_from_verification.expect("Error in verification result");
 
     let expected_public_key_path = format!("{}.der", apk_path.as_ref().to_str().unwrap());
     assert_bytes_eq_to_data_in_file(&public_key_from_verification, expected_public_key_path);
 
-    let public_key_from_apk = get_public_key_der(&apk_path);
+    let public_key_from_apk = get_public_key_der(&apk_path, SDK_INT);
     let public_key_from_apk =
         public_key_from_apk.expect("Error when extracting public key from apk");
     assert_eq!(
@@ -226,15 +283,17 @@
 fn validate_apk_digest<P: AsRef<Path>>(apk_path: P, expected_algorithm_id: SignatureAlgorithmID) {
     let apk = fs::File::open(&apk_path).expect("Unabled to open apk file");
 
-    let (verified_algorithm_id, verified_digest) = get_apk_digest(&apk, /*verify=*/ true)
-        .expect("Error when extracting apk digest with verification.");
+    let (verified_algorithm_id, verified_digest) =
+        get_apk_digest(&apk, SDK_INT, /*verify=*/ true)
+            .expect("Error when extracting apk digest with verification.");
 
     assert_eq!(expected_algorithm_id, verified_algorithm_id);
     let expected_digest_path = format!("{}.apk_digest", apk_path.as_ref().to_str().unwrap());
     assert_bytes_eq_to_data_in_file(&verified_digest, expected_digest_path);
 
-    let (unverified_algorithm_id, unverified_digest) = get_apk_digest(&apk, /*verify=*/ false)
-        .expect("Error when extracting apk digest without verification.");
+    let (unverified_algorithm_id, unverified_digest) =
+        get_apk_digest(&apk, SDK_INT, /*verify=*/ false)
+            .expect("Error when extracting apk digest without verification.");
     assert_eq!(expected_algorithm_id, unverified_algorithm_id);
     assert_eq!(verified_digest, unverified_digest);
 }
diff --git a/libs/apkverify/tests/data/v31-rsa-2048_2-tgt-33-1-tgt-28.apk b/libs/apkverify/tests/data/v31-rsa-2048_2-tgt-33-1-tgt-28.apk
new file mode 100644
index 0000000..aeaec33
--- /dev/null
+++ b/libs/apkverify/tests/data/v31-rsa-2048_2-tgt-33-1-tgt-28.apk
Binary files differ
diff --git a/libs/avb/Android.bp b/libs/avb/Android.bp
index c173f1c..7bfea3f 100644
--- a/libs/avb/Android.bp
+++ b/libs/avb/Android.bp
@@ -2,9 +2,8 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-rust_bindgen {
-    name: "libavb_bindgen",
-    host_supported: true,
+rust_defaults {
+    name: "libavb_bindgen.defaults",
     wrapper_src: "bindgen/avb.h",
     crate_name: "avb_bindgen",
     edition: "2021",
@@ -19,13 +18,28 @@
         "--raw-line=#![no_std]",
         "--ctypes-prefix=core::ffi",
     ],
+    cflags: ["-DBORINGSSL_NO_CXX"],
+}
+
+rust_bindgen {
+    name: "libavb_bindgen",
+    defaults: ["libavb_bindgen.defaults"],
+    host_supported: true,
     static_libs: [
-        "libavb_non_debug",
+        "libavb",
     ],
     shared_libs: [
         "libcrypto",
     ],
-    cflags: ["-DBORINGSSL_NO_CXX"],
+}
+
+rust_bindgen {
+    name: "libavb_bindgen_nostd",
+    defaults: ["libavb_bindgen.defaults"],
+    static_libs: [
+        "libavb_baremetal",
+        "libcrypto_baremetal",
+    ],
 }
 
 rust_test {
diff --git a/libs/devicemapper/src/crypt.rs b/libs/devicemapper/src/crypt.rs
index b2e677a..8281b34 100644
--- a/libs/devicemapper/src/crypt.rs
+++ b/libs/devicemapper/src/crypt.rs
@@ -76,7 +76,7 @@
     device_path: Option<&'a Path>,
     offset: u64,
     device_size: u64,
-    // TODO(b/238179332) Extend this to include opt_params, in particular 'integrity'
+    opt_params: Vec<&'a str>,
 }
 
 impl<'a> Default for DmCryptTargetBuilder<'a> {
@@ -88,6 +88,7 @@
             device_path: None,
             offset: 0,
             device_size: 0,
+            opt_params: Vec::new(),
         }
     }
 }
@@ -124,6 +125,12 @@
         self
     }
 
+    /// Add additional optional parameter
+    pub fn opt_param(&mut self, param: &'a str) -> &mut Self {
+        self.opt_params.push(param);
+        self
+    }
+
     /// Constructs a `DmCryptTarget`.
     pub fn build(&self) -> Result<DmCryptTarget> {
         // The `DmCryptTarget` struct actually is a flattened data consisting of a header and
@@ -154,6 +161,7 @@
         write!(&mut body, "{} ", self.iv_offset)?;
         write!(&mut body, "{} ", device_path)?;
         write!(&mut body, "{} ", self.offset)?;
+        write!(&mut body, "{} {} ", self.opt_params.len(), self.opt_params.join(" "))?;
         write!(&mut body, "\0")?; // null terminator
 
         let size = size_of::<DmTargetSpec>() + body.len();
diff --git a/libs/dice/Android.bp b/libs/dice/Android.bp
deleted file mode 100644
index 8017cff..0000000
--- a/libs/dice/Android.bp
+++ /dev/null
@@ -1,23 +0,0 @@
-package {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-rust_library_rlib {
-    name: "libdice_nostd",
-    crate_name: "dice",
-    srcs: ["src/lib.rs"],
-    edition: "2021",
-    no_stdlibs: true,
-    prefer_rlib: true,
-    stdlibs: ["libcore.rust_sysroot"],
-    rustlibs: [
-        "libopen_dice_cbor_bindgen_nostd",
-        "libopen_dice_bcc_bindgen_nostd",
-    ],
-    whole_static_libs: [
-        "libopen_dice_bcc",
-        "libopen_dice_cbor",
-        "libcrypto_baremetal",
-    ],
-    apex_available: ["com.android.virt"],
-}
diff --git a/libs/dice/src/bcc.rs b/libs/dice/src/bcc.rs
deleted file mode 100644
index 6dc0cc3..0000000
--- a/libs/dice/src/bcc.rs
+++ /dev/null
@@ -1,170 +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.
- */
-
-//! Wrapper around dice/android/bcc.h.
-
-use core::ffi::CStr;
-use core::mem;
-use core::ptr;
-
-use open_dice_bcc_bindgen::BccConfigValues;
-use open_dice_bcc_bindgen::BccFormatConfigDescriptor;
-use open_dice_bcc_bindgen::BccHandoverMainFlow;
-use open_dice_bcc_bindgen::BccHandoverParse;
-use open_dice_bcc_bindgen::DiceInputValues;
-use open_dice_bcc_bindgen::BCC_INPUT_COMPONENT_NAME;
-use open_dice_bcc_bindgen::BCC_INPUT_COMPONENT_VERSION;
-use open_dice_bcc_bindgen::BCC_INPUT_RESETTABLE;
-
-use crate::check_call;
-use crate::Cdi;
-use crate::Error;
-use crate::InputValues;
-use crate::Result;
-
-/// Boot Chain Certificate handover format combining the BCC and CDIs in a single CBOR object.
-#[derive(Clone, Debug)]
-pub struct Handover<'a> {
-    buffer: &'a [u8],
-    /// Attestation CDI.
-    pub cdi_attest: &'a Cdi,
-    /// Sealing CDI.
-    pub cdi_seal: &'a Cdi,
-    /// Boot Chain Certificate (optional).
-    pub bcc: Option<&'a [u8]>,
-}
-
-impl<'a> Handover<'a> {
-    /// Validates and extracts the fields of a BCC handover buffer.
-    pub fn new(buffer: &'a [u8]) -> Result<Self> {
-        let mut cdi_attest: *const u8 = ptr::null();
-        let mut cdi_seal: *const u8 = ptr::null();
-        let mut bcc: *const u8 = ptr::null();
-        let mut bcc_size: usize = 0;
-
-        // SAFETY - The buffer is only read and never stored and the returned pointers should all
-        // point within the address range of the buffer or be NULL.
-        check_call(unsafe {
-            BccHandoverParse(
-                buffer.as_ptr(),
-                buffer.len(),
-                &mut cdi_attest as *mut *const u8,
-                &mut cdi_seal as *mut *const u8,
-                &mut bcc as *mut *const u8,
-                &mut bcc_size as *mut usize,
-            )
-        })?;
-
-        let cdi_attest = {
-            let i = index_from_ptr(buffer, cdi_attest).ok_or(Error::PlatformError)?;
-            let s = buffer.get(i..(i + mem::size_of::<Cdi>())).ok_or(Error::PlatformError)?;
-            s.try_into().map_err(|_| Error::PlatformError)?
-        };
-        let cdi_seal = {
-            let i = index_from_ptr(buffer, cdi_seal).ok_or(Error::PlatformError)?;
-            let s = buffer.get(i..(i + mem::size_of::<Cdi>())).ok_or(Error::PlatformError)?;
-            s.try_into().map_err(|_| Error::PlatformError)?
-        };
-        let bcc = if bcc.is_null() {
-            None
-        } else {
-            let i = index_from_ptr(buffer, bcc).ok_or(Error::PlatformError)?;
-            Some(buffer.get(i..(i + bcc_size)).ok_or(Error::PlatformError)?)
-        };
-
-        Ok(Self { buffer, cdi_attest, cdi_seal, bcc })
-    }
-
-    /// Executes the main BCC handover flow.
-    pub fn main_flow(&self, input_values: &InputValues, buffer: &mut [u8]) -> Result<usize> {
-        let context = ptr::null_mut();
-        let mut size: usize = 0;
-        // SAFETY - The function only reads `self.buffer`, writes to `buffer` within its bounds,
-        // reads `input_values` as a constant input and doesn't store any pointer.
-        check_call(unsafe {
-            BccHandoverMainFlow(
-                context,
-                self.buffer.as_ptr(),
-                self.buffer.len(),
-                input_values as *const _ as *const DiceInputValues,
-                buffer.len(),
-                buffer.as_mut_ptr(),
-                &mut size as *mut usize,
-            )
-        })?;
-
-        Ok(size)
-    }
-}
-
-/// Formats a configuration descriptor following the BCC's specification.
-///
-/// ```
-/// BccConfigDescriptor = {
-///   ? -70002 : tstr,     ; Component name
-///   ? -70003 : int,      ; Component version
-///   ? -70004 : null,     ; Resettable
-/// }
-/// ```
-pub fn format_config_descriptor(
-    buffer: &mut [u8],
-    name: Option<&CStr>,
-    version: Option<u64>,
-    resettable: bool,
-) -> Result<usize> {
-    let mut inputs = 0;
-
-    if name.is_some() {
-        inputs |= BCC_INPUT_COMPONENT_NAME;
-    }
-
-    if version.is_some() {
-        inputs |= BCC_INPUT_COMPONENT_VERSION;
-    }
-
-    if resettable {
-        inputs |= BCC_INPUT_RESETTABLE;
-    }
-
-    let values = BccConfigValues {
-        inputs,
-        component_name: name.map_or(ptr::null(), |p| p.as_ptr()),
-        component_version: version.unwrap_or(0),
-    };
-
-    let mut buffer_size = 0;
-
-    // SAFETY - The function writes to the buffer, within the given bounds, and only reads the
-    // input values. It writes its result to buffer_size.
-    check_call(unsafe {
-        BccFormatConfigDescriptor(
-            &values as *const _,
-            buffer.len(),
-            buffer.as_mut_ptr(),
-            &mut buffer_size as *mut _,
-        )
-    })?;
-
-    Ok(buffer_size)
-}
-
-fn index_from_ptr(slice: &[u8], pointer: *const u8) -> Option<usize> {
-    if slice.as_ptr_range().contains(&pointer) {
-        (pointer as usize).checked_sub(slice.as_ptr() as usize)
-    } else {
-        None
-    }
-}
diff --git a/libs/dice/src/lib.rs b/libs/dice/src/lib.rs
deleted file mode 100644
index 5332092..0000000
--- a/libs/dice/src/lib.rs
+++ /dev/null
@@ -1,160 +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.
- */
-
-//! Bare metal wrapper around libopen_dice.
-
-#![no_std]
-
-use core::fmt;
-use core::mem;
-use core::ptr;
-use core::result;
-
-pub use open_dice_cbor_bindgen::DiceMode;
-
-use open_dice_cbor_bindgen::DiceConfigType_kDiceConfigTypeDescriptor as DICE_CONFIG_TYPE_DESCRIPTOR;
-use open_dice_cbor_bindgen::DiceConfigType_kDiceConfigTypeInline as DICE_CONFIG_TYPE_INLINE;
-use open_dice_cbor_bindgen::DiceHash;
-use open_dice_cbor_bindgen::DiceInputValues;
-use open_dice_cbor_bindgen::DiceResult;
-use open_dice_cbor_bindgen::DiceResult_kDiceResultBufferTooSmall as DICE_RESULT_BUFFER_TOO_SMALL;
-use open_dice_cbor_bindgen::DiceResult_kDiceResultInvalidInput as DICE_RESULT_INVALID_INPUT;
-use open_dice_cbor_bindgen::DiceResult_kDiceResultOk as DICE_RESULT_OK;
-use open_dice_cbor_bindgen::DiceResult_kDiceResultPlatformError as DICE_RESULT_PLATFORM_ERROR;
-
-pub mod bcc;
-
-const CDI_SIZE: usize = open_dice_cbor_bindgen::DICE_CDI_SIZE as usize;
-const HASH_SIZE: usize = open_dice_cbor_bindgen::DICE_HASH_SIZE as usize;
-const HIDDEN_SIZE: usize = open_dice_cbor_bindgen::DICE_HIDDEN_SIZE as usize;
-const INLINE_CONFIG_SIZE: usize = open_dice_cbor_bindgen::DICE_INLINE_CONFIG_SIZE as usize;
-
-/// Array type of CDIs.
-pub type Cdi = [u8; CDI_SIZE];
-/// Array type of hashes used by DICE.
-pub type Hash = [u8; HASH_SIZE];
-/// Array type of additional input.
-pub type Hidden = [u8; HIDDEN_SIZE];
-/// Array type of inline configuration values.
-pub type InlineConfig = [u8; INLINE_CONFIG_SIZE];
-
-/// Error type used by DICE.
-pub enum Error {
-    /// Provided input was invalid.
-    InvalidInput,
-    /// Provided buffer was too small.
-    BufferTooSmall,
-    /// Unexpected platform error.
-    PlatformError,
-    /// Unexpected return value.
-    Unknown(DiceResult),
-}
-
-impl fmt::Debug for Error {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        match self {
-            Error::InvalidInput => write!(f, "invalid input"),
-            Error::BufferTooSmall => write!(f, "buffer too small"),
-            Error::PlatformError => write!(f, "platform error"),
-            Error::Unknown(n) => write!(f, "unknown error: {}", n),
-        }
-    }
-}
-
-/// Result of DICE functions.
-pub type Result<T> = result::Result<T, Error>;
-
-fn check_call(ret: DiceResult) -> Result<()> {
-    match ret {
-        DICE_RESULT_OK => Ok(()),
-        DICE_RESULT_INVALID_INPUT => Err(Error::InvalidInput),
-        DICE_RESULT_BUFFER_TOO_SMALL => Err(Error::BufferTooSmall),
-        DICE_RESULT_PLATFORM_ERROR => Err(Error::PlatformError),
-        n => Err(Error::Unknown(n)),
-    }
-}
-
-/// DICE configuration input type.
-#[derive(Debug)]
-pub enum ConfigType<'a> {
-    /// Uses the formatted 64-byte configuration input value (See the Open Profile for DICE).
-    Inline(InlineConfig),
-    /// Uses the 64-byte hash of more configuration data.
-    Descriptor(&'a [u8]),
-}
-
-/// Set of DICE inputs.
-#[repr(transparent)]
-#[derive(Clone, Debug)]
-pub struct InputValues(DiceInputValues);
-
-impl InputValues {
-    /// Wrap the DICE inputs in a InputValues, expected by bcc::main_flow().
-    pub fn new(
-        code_hash: &Hash,
-        code_descriptor: Option<&[u8]>,
-        config: &ConfigType,
-        auth_hash: Option<&Hash>,
-        auth_descriptor: Option<&[u8]>,
-        mode: DiceMode,
-        hidden: Option<&Hidden>,
-    ) -> Self {
-        const ZEROED_INLINE_CONFIG: InlineConfig = [0; INLINE_CONFIG_SIZE];
-        let (config_type, config_value, config_descriptor) = match config {
-            ConfigType::Inline(value) => (DICE_CONFIG_TYPE_INLINE, *value, None),
-            ConfigType::Descriptor(desc) => {
-                (DICE_CONFIG_TYPE_DESCRIPTOR, ZEROED_INLINE_CONFIG, Some(*desc))
-            }
-        };
-        let (code_descriptor, code_descriptor_size) = as_raw_parts(code_descriptor);
-        let (config_descriptor, config_descriptor_size) = as_raw_parts(config_descriptor);
-        let (authority_descriptor, authority_descriptor_size) = as_raw_parts(auth_descriptor);
-
-        Self(DiceInputValues {
-            code_hash: *code_hash,
-            code_descriptor,
-            code_descriptor_size,
-            config_type,
-            config_value,
-            config_descriptor,
-            config_descriptor_size,
-            authority_hash: auth_hash.map_or([0; mem::size_of::<Hash>()], |h| *h),
-            authority_descriptor,
-            authority_descriptor_size,
-            mode,
-            hidden: hidden.map_or([0; mem::size_of::<Hidden>()], |h| *h),
-        })
-    }
-}
-
-fn ctx() -> *mut core::ffi::c_void {
-    core::ptr::null_mut()
-}
-
-/// Hash the provided input using DICE's default hash function.
-pub fn hash(bytes: &[u8]) -> Result<Hash> {
-    let mut output: Hash = [0; HASH_SIZE];
-    // SAFETY - DiceHash takes a sized input buffer and writes to a constant-sized output buffer.
-    check_call(unsafe { DiceHash(ctx(), bytes.as_ptr(), bytes.len(), output.as_mut_ptr()) })?;
-    Ok(output)
-}
-
-fn as_raw_parts<T: Sized>(s: Option<&[T]>) -> (*const T, usize) {
-    match s {
-        Some(s) => (s.as_ptr(), s.len()),
-        None => (ptr::null(), 0),
-    }
-}
diff --git a/libs/hypervisor_props/Android.bp b/libs/hypervisor_props/Android.bp
new file mode 100644
index 0000000..af08b01
--- /dev/null
+++ b/libs/hypervisor_props/Android.bp
@@ -0,0 +1,18 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_library {
+    name: "libhypervisor_props",
+    crate_name: "hypervisor_props",
+    srcs: ["src/lib.rs"],
+    edition: "2021",
+    rustlibs: [
+        "libanyhow",
+        "librustutils",
+    ],
+    apex_available: [
+        "com.android.compos",
+        "com.android.virt",
+    ],
+}
diff --git a/libs/hypervisor_props/src/lib.rs b/libs/hypervisor_props/src/lib.rs
new file mode 100644
index 0000000..120a48c
--- /dev/null
+++ b/libs/hypervisor_props/src/lib.rs
@@ -0,0 +1,40 @@
+// Copyright 2023, 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.
+
+//! Access to hypervisor capabilities via system properties set by the bootloader.
+
+use anyhow::{Error, Result};
+use rustutils::system_properties;
+
+/// Returns whether there is a hypervisor present that supports non-protected VMs.
+pub fn is_vm_supported() -> Result<bool> {
+    system_properties::read_bool("ro.boot.hypervisor.vm.supported", false).map_err(Error::new)
+}
+
+/// Returns whether there is a hypervisor present that supports protected VMs.
+pub fn is_protected_vm_supported() -> Result<bool> {
+    system_properties::read_bool("ro.boot.hypervisor.protected_vm.supported", false)
+        .map_err(Error::new)
+}
+
+/// Returns whether there is a hypervisor present that supports any sort of VM, either protected
+/// or non-protected.
+pub fn is_any_vm_supported() -> Result<bool> {
+    is_vm_supported().and_then(|ok| if ok { Ok(true) } else { is_protected_vm_supported() })
+}
+
+/// Returns the version of the hypervisor, if there is one.
+pub fn version() -> Result<Option<String>> {
+    system_properties::read("ro.boot.hypervisor.version").map_err(Error::new)
+}
diff --git a/libs/libfdt/src/lib.rs b/libs/libfdt/src/lib.rs
index 29d7abe..1d295eb 100644
--- a/libs/libfdt/src/lib.rs
+++ b/libs/libfdt/src/lib.rs
@@ -405,6 +405,24 @@
         fdt_err_expect_zero(ret)
     }
 
+    /// Create or change a flag-like empty property.
+    pub fn setprop_empty(&mut self, name: &CStr) -> Result<()> {
+        self.setprop(name, &[])
+    }
+
+    /// Delete the given property.
+    pub fn delprop(&mut self, name: &CStr) -> Result<()> {
+        // SAFETY - Accesses are constrained to the DT totalsize (validated by ctor) when the
+        // library locates the node's property. Removing the property may shift the offsets of
+        // other nodes and properties but the borrow checker should prevent this function from
+        // being called when FdtNode instances are in use.
+        let ret = unsafe {
+            libfdt_bindgen::fdt_delprop(self.fdt.as_mut_ptr(), self.offset, name.as_ptr())
+        };
+
+        fdt_err_expect_zero(ret)
+    }
+
     /// Get reference to the containing device tree.
     pub fn fdt(&mut self) -> &mut Fdt {
         self.fdt
@@ -561,6 +579,11 @@
         self.node(CStr::from_bytes_with_nul(b"/chosen\0").unwrap())
     }
 
+    /// Retrieve the standard /chosen node as mutable.
+    pub fn chosen_mut(&mut self) -> Result<Option<FdtNodeMut>> {
+        self.node_mut(CStr::from_bytes_with_nul(b"/chosen\0").unwrap())
+    }
+
     /// Get the root node of the tree.
     pub fn root(&self) -> Result<FdtNode> {
         self.node(CStr::from_bytes_with_nul(b"/\0").unwrap())?.ok_or(FdtError::Internal)
@@ -614,7 +637,8 @@
         fdt_err_expect_zero(ret)
     }
 
-    fn as_ptr(&self) -> *const c_void {
+    /// Return a shared pointer to the device tree.
+    pub fn as_ptr(&self) -> *const c_void {
         self as *const _ as *const c_void
     }
 
diff --git a/microdroid/Android.bp b/microdroid/Android.bp
index 9264692..a2a4138 100644
--- a/microdroid/Android.bp
+++ b/microdroid/Android.bp
@@ -51,6 +51,7 @@
     deps: [
         "init_second_stage",
         "microdroid_build_prop",
+        "microdroid_init_debug_policy",
         "microdroid_init_rc",
         "microdroid_ueventd_rc",
         "microdroid_launcher",
@@ -85,10 +86,6 @@
         "microdroid_property_contexts",
         "mke2fs.microdroid",
 
-        // TODO(b/195425111) these should be added automatically
-        "libcrypto", // used by many (init_second_stage, microdroid_manager, toybox, etc)
-        "liblzma", // used by init_second_stage
-
         "libvm_payload", // used by payload to interact with microdroid manager
 
         "prng_seeder_microdroid",
diff --git a/microdroid/init.rc b/microdroid/init.rc
index ce0cab4..5187a12 100644
--- a/microdroid/init.rc
+++ b/microdroid/init.rc
@@ -21,13 +21,9 @@
     write /linkerconfig/ld.config.txt \#
     chmod 644 /linkerconfig/ld.config.txt
 
-# If VM is debuggable, send logs to outside ot the VM via the serial console.
-# If non-debuggable, logs are internally consumed at /dev/null
-on early-init && property:ro.boot.microdroid.debuggable=1
-    setprop ro.log.file_logger.path /dev/hvc2
-
-on early-init && property:ro.boot.microdroid.debuggable=0
-    setprop ro.log.file_logger.path /dev/null
+    # Applies debug policy to decide whether to enable adb, adb root, and logcat.
+    # We don't directly exec the binary to specify stdio_to_kmsg.
+    exec_start init_debug_policy
 
 on init
     mkdir /mnt/apk 0755 system system
@@ -47,8 +43,6 @@
     # payloads are not designed to run with bootstrap bionic
     setprop apex_config.done true
 
-    setprop ro.debuggable ${ro.boot.microdroid.debuggable:-0}
-
 on property:microdroid_manager.init_done=1
     # Stop ueventd to save memory
     stop ueventd
@@ -57,7 +51,7 @@
     # Mount tracefs (with GID=AID_READTRACEFS)
     mount tracefs tracefs /sys/kernel/tracing gid=3012
 
-on init && property:ro.boot.adb.enabled=1
+on property:init_debug_policy.adbd.enabled=1
     start adbd
 
 # Mount filesystems and start core system services.
@@ -98,7 +92,7 @@
     mount rootfs rootfs / remount bind ro nodev
 
     # TODO(b/185767624): change the hard-coded size?
-    mount tmpfs tmpfs /data noatime nosuid nodev rw size=128M
+    mount tmpfs tmpfs /data noatime nosuid nodev noexec rw size=128M
 
     # We chown/chmod /data again so because mount is run as root + defaults
     chown system system /data
@@ -179,3 +173,8 @@
     group shell log readproc
     seclabel u:r:shell:s0
     setenv HOSTNAME console
+
+service init_debug_policy /system/bin/init_debug_policy
+    oneshot
+    disabled
+    stdio_to_kmsg
diff --git a/microdroid/init_debug_policy/Android.bp b/microdroid/init_debug_policy/Android.bp
new file mode 100644
index 0000000..b56ef79
--- /dev/null
+++ b/microdroid/init_debug_policy/Android.bp
@@ -0,0 +1,11 @@
+rust_binary {
+    name: "microdroid_init_debug_policy",
+    srcs: ["src/init_debug_policy.rs"],
+    stem: "init_debug_policy",
+    rustlibs: [
+        "librustutils",
+    ],
+    installable: false, // match with microdroid_init_rc.
+    bootstrap: true,
+    prefer_rlib: true,
+}
diff --git a/microdroid/init_debug_policy/src/init_debug_policy.rs b/microdroid/init_debug_policy/src/init_debug_policy.rs
new file mode 100644
index 0000000..6c80926
--- /dev/null
+++ b/microdroid/init_debug_policy/src/init_debug_policy.rs
@@ -0,0 +1,57 @@
+// Copyright 2023, 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.
+
+//! Applies debug policies when booting microdroid
+
+use rustutils::system_properties;
+use rustutils::system_properties::PropertyWatcherError;
+use std::fs::File;
+use std::io::Read;
+
+/// Get debug policy value in bool. It's true iff the value is explicitly set to <1>.
+fn get_debug_policy_bool(path: &'static str) -> Option<bool> {
+    let mut file = File::open(path).ok()?;
+    let mut log: [u8; 4] = Default::default();
+    file.read_exact(&mut log).ok()?;
+    // DT spec uses big endian although Android is always little endian.
+    Some(u32::from_be_bytes(log) == 1)
+}
+
+fn main() -> Result<(), PropertyWatcherError> {
+    // If VM is debuggable or debug policy says so, send logs to outside ot the VM via the serial console.
+    // Otherwise logs are internally consumed at /dev/null
+    let log_path = if system_properties::read_bool("ro.boot.microdroid.debuggable", false)?
+        || get_debug_policy_bool("/sys/firmware/devicetree/base/avf/guest/common/log")
+            .unwrap_or_default()
+    {
+        "/dev/hvc2"
+    } else {
+        "/dev/null"
+    };
+    system_properties::write("ro.log.file_logger.path", log_path)?;
+
+    let (adbd_enabled, debuggable) = if system_properties::read_bool("ro.boot.adb.enabled", false)?
+        || get_debug_policy_bool("/sys/firmware/devicetree/base/avf/guest/microdroid/adb")
+            .unwrap_or_default()
+    {
+        // debuggable is required for adb root and bypassing adb authorization.
+        ("1", "1")
+    } else {
+        ("0", "0")
+    };
+    system_properties::write("init_debug_policy.adbd.enabled", adbd_enabled)?;
+    system_properties::write("ro.debuggable", debuggable)?;
+
+    Ok(())
+}
diff --git a/microdroid/initrd/src/main.rs b/microdroid/initrd/src/main.rs
index c5515af..8f06f09 100644
--- a/microdroid/initrd/src/main.rs
+++ b/microdroid/initrd/src/main.rs
@@ -90,7 +90,7 @@
 
     let initrd_size: usize = initrd_bc_size - bc_size - INITRD_FOOTER_LEN;
 
-    initrd_bc.seek(SeekFrom::Start(0))?;
+    initrd_bc.rewind()?;
     copyfile2file(&mut initrd_bc, &mut initrd, initrd_size)?;
     copyfile2file(&mut initrd_bc, &mut bootconfig, bc_size)?;
     Ok(())
diff --git a/microdroid/kdump/kernel/arm64/kernel-5.15 b/microdroid/kdump/kernel/arm64/kernel-5.15
index 0f2172b..28b0214 100644
--- a/microdroid/kdump/kernel/arm64/kernel-5.15
+++ b/microdroid/kdump/kernel/arm64/kernel-5.15
Binary files differ
diff --git a/microdroid/payload/Android.bp b/microdroid/payload/Android.bp
index f77c037..4814a64 100644
--- a/microdroid/payload/Android.bp
+++ b/microdroid/payload/Android.bp
@@ -36,26 +36,13 @@
     ],
 }
 
-cc_binary_host {
-    name: "mk_payload",
+java_library_host {
+    name: "microdroid_payload_metadata",
     srcs: [
-        "mk_payload.cc",
+        "src/**/*.java",
+        "metadata.proto",
     ],
-    static_libs: [
-        "lib_microdroid_metadata_proto",
-        "libbase",
-        "libcdisk_spec",
-        "libcuttlefish_fs",
-        "libcuttlefish_utils",
-        "libext2_uuid",
-        "libimage_aggregator",
-        "libjsoncpp",
-        "liblog",
-        "libprotobuf-cpp-full",
-        "libprotobuf-cpp-lite",
-        "libsparse",
-        "libxml2",
-        "libz",
-    ],
-    static_executable: true,
+    proto: {
+        type: "lite",
+    },
 }
diff --git a/microdroid/payload/README.md b/microdroid/payload/README.md
index c2f624a..b1eb63f 100644
--- a/microdroid/payload/README.md
+++ b/microdroid/payload/README.md
@@ -38,34 +38,3 @@
 Each payload partition presents APEX or APK passed from the host.
 
 The size of a payload partition must be a multiple of 4096 bytes.
-
-# `mk_payload`
-
-`mk_payload` is a small utility to create a payload disk image. It is used by ARCVM.
-
-```
-$ cat payload_config.json
-{
-  "apexes": [
-    {
-      "name": "com.my.hello",
-      "path": "hello.apex",
-    }
-  ],
-  "apk": {
-    "name": "com.my.world",
-    "path": "/path/to/world.apk",
-    "idsigPath": "/path/to/world.apk.idsig",
-  }
-}
-$ m mk_payload
-$ mk_payload payload_config.json payload.img
-$ ls
-payload.img
-payload-footer.img
-payload-header.img
-payload-metadata.img
-payload-filler-0.img
-payload-filler-1.img
-...
-```
diff --git a/microdroid/payload/config/src/lib.rs b/microdroid/payload/config/src/lib.rs
index 08b8b42..cdef3e4 100644
--- a/microdroid/payload/config/src/lib.rs
+++ b/microdroid/payload/config/src/lib.rs
@@ -40,8 +40,8 @@
     pub prefer_staged: bool,
 
     /// Whether to export the tomsbtones (VM crashes) out of VM to host
-    /// This does not have a default & the value is expected to be in json for deserialization
-    pub export_tombstones: bool,
+    /// Default: true for debuggable VMs, false for non-debuggable VMs
+    pub export_tombstones: Option<bool>,
 
     /// Whether the authfs service should be started in the VM. This enables read or write of host
     /// files with integrity checking, but not confidentiality.
@@ -64,10 +64,11 @@
 
 /// Payload's task can be one of plain executable
 /// or an .so library which can be started via /system/bin/microdroid_launcher
-#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Default)]
 pub enum TaskType {
     /// Task's command indicates the path to the executable binary.
     #[serde(rename = "executable")]
+    #[default]
     Executable,
     /// Task's command indicates the .so library in /mnt/apk/lib/{arch}
     #[serde(rename = "microdroid_launcher")]
@@ -87,12 +88,6 @@
     pub command: String,
 }
 
-impl Default for TaskType {
-    fn default() -> TaskType {
-        TaskType::Executable
-    }
-}
-
 /// APEX config
 /// For now, we only pass the name of APEX.
 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
diff --git a/microdroid/payload/metadata.proto b/microdroid/payload/metadata.proto
index c74c23b..6b999af 100644
--- a/microdroid/payload/metadata.proto
+++ b/microdroid/payload/metadata.proto
@@ -18,6 +18,9 @@
 
 package android.microdroid;
 
+option java_package = "com.android.virt";
+option java_outer_classname = "PayloadMetadataProtos";
+
 // Metadata is the body of the "metadata" partition
 message Metadata {
   uint32 version = 1;
diff --git a/microdroid/payload/src/com/android/virt/PayloadMetadata.java b/microdroid/payload/src/com/android/virt/PayloadMetadata.java
new file mode 100644
index 0000000..c2f0a7f
--- /dev/null
+++ b/microdroid/payload/src/com/android/virt/PayloadMetadata.java
@@ -0,0 +1,49 @@
+package com.android.virt;
+
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+/** Provides utility to create/read/write PayloadMetadata */
+public class PayloadMetadata {
+    public static void write(PayloadMetadataProtos.Metadata metadata, File file)
+            throws IOException {
+        byte[] message = metadata.toByteArray();
+
+        try (DataOutputStream os = new DataOutputStream(new FileOutputStream(file))) {
+            // write length prefix (4-byte, big-endian)
+            os.writeInt(message.length);
+            // write the message
+            os.write(message);
+        }
+    }
+
+    public static PayloadMetadataProtos.Metadata metadata(
+            String configPath,
+            PayloadMetadataProtos.ApkPayload apk,
+            Iterable<? extends PayloadMetadataProtos.ApexPayload> apexes) {
+        return PayloadMetadataProtos.Metadata.newBuilder()
+                .setVersion(1)
+                .setConfigPath(configPath)
+                .setApk(apk)
+                .addAllApexes(apexes)
+                .build();
+    }
+
+    public static PayloadMetadataProtos.ApkPayload apk(String name) {
+        return PayloadMetadataProtos.ApkPayload.newBuilder()
+                .setName(name)
+                .setPayloadPartitionName("microdroid-apk")
+                .setIdsigPartitionName("microdroid-apk-idsig")
+                .build();
+    }
+
+    public static PayloadMetadataProtos.ApexPayload apex(String name) {
+        return PayloadMetadataProtos.ApexPayload.newBuilder()
+                .setName(name)
+                .setIsFactory(true)
+                .setPartitionName(name)
+                .build();
+    }
+}
diff --git a/microdroid_manager/Android.bp b/microdroid_manager/Android.bp
index 383f371..495d3bb 100644
--- a/microdroid_manager/Android.bp
+++ b/microdroid_manager/Android.bp
@@ -19,10 +19,9 @@
         "libbinder_rs",
         "libbyteorder",
         "libcap_rust",
-        "libdiced",
-        "libdiced_open_dice_cbor",
+        "libciborium",
+        "libdiced_open_dice",
         "libdiced_sample_inputs",
-        "libdiced_utils",
         "libglob",
         "libhex",
         "libitertools",
@@ -47,9 +46,6 @@
         "libvsock",
         "librand",
     ],
-    shared_libs: [
-        "libbinder_rpc_unstable",
-    ],
     init_rc: ["microdroid_manager.rc"],
     multilib: {
         lib32: {
diff --git a/microdroid_manager/src/dice.rs b/microdroid_manager/src/dice.rs
index e740ed4..3a2a1e6 100644
--- a/microdroid_manager/src/dice.rs
+++ b/microdroid_manager/src/dice.rs
@@ -14,14 +14,16 @@
 
 //! Logic for handling the DICE values and boot operations.
 
-use anyhow::{bail, Context, Error, Result};
+use anyhow::{anyhow, bail, Context, Error, Result};
 use byteorder::{NativeEndian, ReadBytesExt};
-use diced_open_dice_cbor::{
-    Config, ContextImpl, DiceMode, InputValuesOwned, OpenDiceCborContext, CDI_SIZE, HASH_SIZE,
-    HIDDEN_SIZE,
+use ciborium::{cbor, ser};
+use diced_open_dice::{
+    bcc_handover_parse, retry_bcc_main_flow, BccHandover, Config, DiceArtifacts, DiceMode, Hash,
+    Hidden, InputValues, OwnedDiceArtifacts,
 };
 use keystore2_crypto::ZVec;
 use libc::{c_void, mmap, munmap, MAP_FAILED, MAP_PRIVATE, PROT_READ};
+use microdroid_metadata::PayloadMetadata;
 use openssl::hkdf::hkdf;
 use openssl::md::Md;
 use std::fs;
@@ -30,22 +32,14 @@
 use std::ptr::null_mut;
 use std::slice;
 
-/// Artifacts that are kept in the process address space after the artifacts from the driver have
-/// been consumed.
-pub struct DiceContext {
-    pub cdi_attest: [u8; CDI_SIZE],
-    pub cdi_seal: [u8; CDI_SIZE],
-    pub bcc: Vec<u8>,
-}
-
-impl DiceContext {
-    pub fn get_sealing_key(&self, salt: &[u8], identifier: &[u8], keysize: u32) -> Result<ZVec> {
-        // Deterministically derive a key to use for sealing data based on salt. Use different salt
-        // for different keys.
-        let mut key = ZVec::new(keysize as usize)?;
-        hkdf(&mut key, Md::sha256(), &self.cdi_seal, salt, identifier)?;
-        Ok(key)
-    }
+/// Derives a sealing key from the DICE sealing CDI.
+pub fn derive_sealing_key(
+    dice_artifacts: &dyn DiceArtifacts,
+    salt: &[u8],
+    info: &[u8],
+    key: &mut [u8],
+) -> Result<()> {
+    Ok(hkdf(key, Md::sha256(), dice_artifacts.cdi_seal(), salt, info)?)
 }
 
 /// Artifacts that are mapped into the process address space from the driver.
@@ -54,14 +48,19 @@
         driver_path: PathBuf,
         mmap_addr: *mut c_void,
         mmap_size: usize,
-        cdi_attest: &'a [u8; CDI_SIZE],
-        cdi_seal: &'a [u8; CDI_SIZE],
-        bcc: &'a [u8],
+        bcc_handover: BccHandover<'a>,
     },
-    Fake(DiceContext),
+    Fake(OwnedDiceArtifacts),
 }
 
 impl DiceDriver<'_> {
+    fn dice_artifacts(&self) -> &dyn DiceArtifacts {
+        match self {
+            Self::Real { bcc_handover, .. } => bcc_handover,
+            Self::Fake(owned_dice_artifacts) => owned_dice_artifacts,
+        }
+    }
+
     pub fn new(driver_path: &Path) -> Result<Self> {
         if driver_path.exists() {
             log::info!("Using DICE values from driver");
@@ -69,13 +68,9 @@
             bail!("Strict boot requires DICE value from driver but none were found");
         } else {
             log::warn!("Using sample DICE values");
-            let (cdi_attest, cdi_seal, bcc) = diced_sample_inputs::make_sample_bcc_and_cdis()
+            let dice_artifacts = diced_sample_inputs::make_sample_bcc_and_cdis()
                 .expect("Failed to create sample dice artifacts.");
-            return Ok(Self::Fake(DiceContext {
-                cdi_attest: cdi_attest[..].try_into().unwrap(),
-                cdi_seal: cdi_seal[..].try_into().unwrap(),
-                bcc,
-            }));
+            return Ok(Self::Fake(dice_artifacts));
         };
 
         let mut file = fs::File::open(driver_path)
@@ -97,78 +92,57 @@
         // accessible and not referenced from anywhere else.
         let mmap_buf =
             unsafe { slice::from_raw_parts((mmap_addr as *const u8).as_ref().unwrap(), mmap_size) };
-        // Very inflexible parsing / validation of the BccHandover data. Assumes deterministically
-        // encoded CBOR.
-        //
-        // BccHandover = {
-        //   1 : bstr .size 32,     ; CDI_Attest
-        //   2 : bstr .size 32,     ; CDI_Seal
-        //   3 : Bcc,               ; Certificate chain
-        // }
-        if mmap_buf[0..4] != [0xa3, 0x01, 0x58, 0x20]
-            || mmap_buf[36..39] != [0x02, 0x58, 0x20]
-            || mmap_buf[71] != 0x03
-        {
-            bail!("BccHandover format mismatch");
-        }
+        let bcc_handover =
+            bcc_handover_parse(mmap_buf).map_err(|_| anyhow!("Failed to parse Bcc Handover"))?;
         Ok(Self::Real {
             driver_path: driver_path.to_path_buf(),
             mmap_addr,
             mmap_size,
-            cdi_attest: mmap_buf[4..36].try_into().unwrap(),
-            cdi_seal: mmap_buf[39..71].try_into().unwrap(),
-            bcc: &mmap_buf[72..],
+            bcc_handover,
         })
     }
 
-    pub fn get_sealing_key(&self, identifier: &[u8]) -> Result<ZVec> {
+    /// Derives a sealing key of `key_length` bytes from the DICE sealing CDI.
+    pub fn get_sealing_key(&self, identifier: &[u8], key_length: usize) -> Result<ZVec> {
         // Deterministically derive a key to use for sealing data, rather than using the CDI
         // directly, so we have the chance to rotate the key if needed. A salt isn't needed as the
         // input key material is already cryptographically strong.
-        let cdi_seal = match self {
-            Self::Real { cdi_seal, .. } => cdi_seal,
-            Self::Fake(fake) => &fake.cdi_seal,
-        };
+        let mut key = ZVec::new(key_length)?;
         let salt = &[];
-        let mut key = ZVec::new(32)?;
-        hkdf(&mut key, Md::sha256(), cdi_seal, salt, identifier)?;
+        derive_sealing_key(self.dice_artifacts(), salt, identifier, &mut key)?;
         Ok(key)
     }
 
     pub fn derive(
         self,
-        code_hash: [u8; HASH_SIZE],
+        code_hash: Hash,
         config_desc: &[u8],
-        authority_hash: [u8; HASH_SIZE],
+        authority_hash: Hash,
         debug: bool,
-        hidden: [u8; HIDDEN_SIZE],
-    ) -> Result<DiceContext> {
-        let input_values = InputValuesOwned::new(
+        hidden: Hidden,
+    ) -> Result<OwnedDiceArtifacts> {
+        let input_values = InputValues::new(
             code_hash,
             Config::Descriptor(config_desc),
             authority_hash,
-            None,
             if debug { DiceMode::kDiceModeDebug } else { DiceMode::kDiceModeNormal },
             hidden,
         );
-        let (cdi_attest, cdi_seal, bcc) = match &self {
-            Self::Real { cdi_attest, cdi_seal, bcc, .. } => (*cdi_attest, *cdi_seal, *bcc),
-            Self::Fake(fake) => (&fake.cdi_attest, &fake.cdi_seal, fake.bcc.as_slice()),
-        };
-        let (cdi_attest, cdi_seal, bcc) = OpenDiceCborContext::new()
-            .bcc_main_flow(cdi_attest, cdi_seal, bcc, &input_values)
-            .context("DICE derive from driver")?;
+        let current_dice_artifacts = self.dice_artifacts();
+        let next_dice_artifacts = retry_bcc_main_flow(
+            current_dice_artifacts.cdi_attest(),
+            current_dice_artifacts.cdi_seal(),
+            current_dice_artifacts.bcc().ok_or_else(|| anyhow!("bcc is none"))?,
+            &input_values,
+        )
+        .context("DICE derive from driver")?;
         if let Self::Real { driver_path, .. } = &self {
             // Writing to the device wipes the artifacts. The string is ignored by the driver but
             // included for documentation.
             fs::write(driver_path, "wipe")
                 .map_err(|err| Error::new(err).context("Wiping driver"))?;
         }
-        Ok(DiceContext {
-            cdi_attest: cdi_attest[..].try_into().unwrap(),
-            cdi_seal: cdi_seal[..].try_into().unwrap(),
-            bcc,
-        })
+        Ok(next_dice_artifacts)
     }
 }
 
@@ -185,3 +159,70 @@
         }
     }
 }
+
+/// Returns a configuration descriptor of the given payload following the BCC's specification:
+/// https://cs.android.com/android/platform/superproject/+/master:hardware/interfaces/security/rkp/aidl/android/hardware/security/keymint/ProtectedData.aidl
+/// {
+///   -70002: "Microdroid payload",
+///   ? -71000: tstr // payload_config_path
+///   ? -71001: PayloadConfig
+/// }
+/// PayloadConfig = {
+///   1: tstr // payload_binary_name
+/// }
+pub fn format_payload_config_descriptor(payload_metadata: &PayloadMetadata) -> Result<Vec<u8>> {
+    const MICRODROID_PAYLOAD_COMPONENT_NAME: &str = "Microdroid payload";
+
+    let config_descriptor_cbor_value = match payload_metadata {
+        PayloadMetadata::config_path(payload_config_path) => cbor!({
+            -70002 => MICRODROID_PAYLOAD_COMPONENT_NAME,
+            -71000 => payload_config_path
+        }),
+        PayloadMetadata::config(payload_config) => cbor!({
+            -70002 => MICRODROID_PAYLOAD_COMPONENT_NAME,
+            -71001 => {1 => payload_config.payload_binary_name}
+        }),
+    }
+    .context("Failed to build a CBOR Value from payload metadata")?;
+    let mut config_descriptor = Vec::new();
+    ser::into_writer(&config_descriptor_cbor_value, &mut config_descriptor)?;
+    Ok(config_descriptor)
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use microdroid_metadata::PayloadConfig;
+
+    #[test]
+    fn payload_metadata_with_path_formats_correctly() -> Result<()> {
+        let payload_metadata = PayloadMetadata::config_path("/config_path".to_string());
+        let config_descriptor = format_payload_config_descriptor(&payload_metadata)?;
+        static EXPECTED_CONFIG_DESCRIPTOR: &[u8] = &[
+            0xa2, 0x3a, 0x00, 0x01, 0x11, 0x71, 0x72, 0x4d, 0x69, 0x63, 0x72, 0x6f, 0x64, 0x72,
+            0x6f, 0x69, 0x64, 0x20, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x3a, 0x00, 0x01,
+            0x15, 0x57, 0x6c, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x70, 0x61, 0x74,
+            0x68,
+        ];
+        assert_eq!(EXPECTED_CONFIG_DESCRIPTOR, &config_descriptor);
+        Ok(())
+    }
+
+    #[test]
+    fn payload_metadata_with_config_formats_correctly() -> Result<()> {
+        let payload_config = PayloadConfig {
+            payload_binary_name: "payload_binary".to_string(),
+            ..Default::default()
+        };
+        let payload_metadata = PayloadMetadata::config(payload_config);
+        let config_descriptor = format_payload_config_descriptor(&payload_metadata)?;
+        static EXPECTED_CONFIG_DESCRIPTOR: &[u8] = &[
+            0xa2, 0x3a, 0x00, 0x01, 0x11, 0x71, 0x72, 0x4d, 0x69, 0x63, 0x72, 0x6f, 0x64, 0x72,
+            0x6f, 0x69, 0x64, 0x20, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x3a, 0x00, 0x01,
+            0x15, 0x58, 0xa1, 0x01, 0x6e, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x62,
+            0x69, 0x6e, 0x61, 0x72, 0x79,
+        ];
+        assert_eq!(EXPECTED_CONFIG_DESCRIPTOR, &config_descriptor);
+        Ok(())
+    }
+}
diff --git a/microdroid_manager/src/instance.rs b/microdroid_manager/src/instance.rs
index 96e9360..6900ea5 100644
--- a/microdroid_manager/src/instance.rs
+++ b/microdroid_manager/src/instance.rs
@@ -142,9 +142,9 @@
         self.file.read_exact(&mut header)?;
 
         // Decrypt and authenticate the data (along with the header).
-        let key = dice.get_sealing_key(INSTANCE_KEY_IDENTIFIER)?;
-        let plaintext =
-            decrypt_aead(Cipher::aes_256_gcm(), &key, Some(&nonce), &header, &data, &tag)?;
+        let cipher = Cipher::aes_256_gcm();
+        let key = dice.get_sealing_key(INSTANCE_KEY_IDENTIFIER, cipher.key_len())?;
+        let plaintext = decrypt_aead(cipher, &key, Some(&nonce), &header, &data, &tag)?;
 
         let microdroid_data = serde_cbor::from_slice(plaintext.as_slice())?;
         Ok(Some(microdroid_data))
@@ -188,10 +188,10 @@
         self.file.write_all(nonce.as_ref())?;
 
         // Then encrypt and sign the data.
-        let key = dice.get_sealing_key(INSTANCE_KEY_IDENTIFIER)?;
+        let cipher = Cipher::aes_256_gcm();
+        let key = dice.get_sealing_key(INSTANCE_KEY_IDENTIFIER, cipher.key_len())?;
         let mut tag = [0; AES_256_GCM_TAG_LENGTH];
-        let ciphertext =
-            encrypt_aead(Cipher::aes_256_gcm(), &key, Some(&nonce), &header, &data, &mut tag)?;
+        let ciphertext = encrypt_aead(cipher, &key, Some(&nonce), &header, &data, &mut tag)?;
 
         // Persist the encrypted payload data and the tag.
         self.file.write_all(&ciphertext)?;
diff --git a/microdroid_manager/src/main.rs b/microdroid_manager/src/main.rs
index 13bc9e3..fa96bf4 100644
--- a/microdroid_manager/src/main.rs
+++ b/microdroid_manager/src/main.rs
@@ -21,7 +21,7 @@
 mod swap;
 mod vm_payload_service;
 
-use crate::dice::{DiceContext, DiceDriver};
+use crate::dice::{DiceDriver, derive_sealing_key, format_payload_config_descriptor};
 use crate::instance::{ApexData, ApkData, InstanceDisk, MicrodroidData, RootHash};
 use crate::vm_payload_service::register_vm_payload_service;
 use android_system_virtualizationcommon::aidl::android::system::virtualizationcommon::ErrorCode::ErrorCode;
@@ -34,11 +34,12 @@
 use anyhow::{anyhow, bail, ensure, Context, Error, Result};
 use apkverify::{get_public_key_der, verify, V4Signature};
 use binder::Strong;
-use diced_utils::cbor::{encode_header, encode_number};
+use diced_open_dice::OwnedDiceArtifacts;
 use glob::glob;
 use itertools::sorted;
 use libc::VMADDR_CID_HOST;
 use log::{error, info, warn};
+use keystore2_crypto::ZVec;
 use microdroid_metadata::{write_metadata, Metadata, PayloadMetadata};
 use microdroid_payload_config::{OsConfig, Task, TaskType, VmPayloadConfig};
 use nix::fcntl::{fcntl, F_SETFD, FdFlag};
@@ -70,13 +71,15 @@
 const EXTRA_APK_PATH_PATTERN: &str = "/dev/block/by-name/extra-apk-*";
 const EXTRA_IDSIG_PATH_PATTERN: &str = "/dev/block/by-name/extra-idsig-*";
 const DM_MOUNTED_APK_PATH: &str = "/dev/block/mapper/microdroid-apk";
-const APKDMVERITY_BIN: &str = "/system/bin/apkdmverity";
-const ZIPFUSE_BIN: &str = "/system/bin/zipfuse";
 const AVF_STRICT_BOOT: &str = "/sys/firmware/devicetree/base/chosen/avf,strict-boot";
 const AVF_NEW_INSTANCE: &str = "/sys/firmware/devicetree/base/chosen/avf,new-instance";
 const DEBUG_MICRODROID_NO_VERIFIED_BOOT: &str =
     "/sys/firmware/devicetree/base/virtualization/guest/debug-microdroid,no-verified-boot";
 
+const APKDMVERITY_BIN: &str = "/system/bin/apkdmverity";
+const ENCRYPTEDSTORE_BIN: &str = "/system/bin/encryptedstore";
+const ZIPFUSE_BIN: &str = "/system/bin/zipfuse";
+
 const APEX_CONFIG_DONE_PROP: &str = "apex_config.done";
 const TOMBSTONE_TRANSMIT_DONE_PROP: &str = "tombstone_transmit.init_done";
 const DEBUGGABLE_PROP: &str = "ro.boot.microdroid.debuggable";
@@ -84,11 +87,9 @@
 // SYNC WITH virtualizationservice/src/crosvm.rs
 const FAILURE_SERIAL_DEVICE: &str = "/dev/ttyS1";
 
-/// Identifier for the key used for encrypted store.
 const ENCRYPTEDSTORE_BACKING_DEVICE: &str = "/dev/block/by-name/encryptedstore";
-const ENCRYPTEDSTORE_BIN: &str = "/system/bin/encryptedstore";
 const ENCRYPTEDSTORE_KEY_IDENTIFIER: &str = "encryptedstore_key";
-const ENCRYPTEDSTORE_KEYSIZE: u32 = 32;
+const ENCRYPTEDSTORE_KEYSIZE: usize = 32;
 
 #[derive(thiserror::Error, Debug)]
 enum MicrodroidError {
@@ -268,7 +269,7 @@
     dice: DiceDriver,
     verified_data: &MicrodroidData,
     payload_metadata: &PayloadMetadata,
-) -> Result<DiceContext> {
+) -> Result<OwnedDiceArtifacts> {
     // Calculate compound digests of code and authorities
     let mut code_hash_ctx = Sha512::new();
     let mut authority_hash_ctx = Sha512::new();
@@ -285,54 +286,14 @@
     let code_hash = code_hash_ctx.finish();
     let authority_hash = authority_hash_ctx.finish();
 
-    // {
-    //   -70002: "Microdroid payload",
-    //   ? -71000: tstr // payload_config_path
-    //   ? -71001: PayloadConfig
-    // }
-    // PayloadConfig = {
-    //   1: tstr // payload_binary_name
-    // }
-
-    let mut config_desc = vec![
-        0xa2, // map(2)
-        0x3a, 0x00, 0x01, 0x11, 0x71, // -70002
-        0x72, 0x4d, 0x69, 0x63, 0x72, 0x6f, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x20, 0x70, 0x61, 0x79,
-        0x6c, 0x6f, 0x61, 0x64, // "Microdroid payload"
-    ];
-
-    match payload_metadata {
-        PayloadMetadata::config_path(payload_config_path) => {
-            encode_negative_number(-71000, &mut config_desc)?;
-            encode_tstr(payload_config_path, &mut config_desc)?;
-        }
-        PayloadMetadata::config(payload_config) => {
-            encode_negative_number(-71001, &mut config_desc)?;
-            encode_header(5, 1, &mut config_desc)?; // map(1)
-            encode_number(1, &mut config_desc)?;
-            encode_tstr(&payload_config.payload_binary_name, &mut config_desc)?;
-        }
-    }
+    let config_descriptor = format_payload_config_descriptor(payload_metadata)?;
 
     // Check debuggability, conservatively assuming it is debuggable
     let debuggable = system_properties::read_bool(DEBUGGABLE_PROP, true)?;
 
     // Send the details to diced
     let hidden = verified_data.salt.clone().try_into().unwrap();
-    dice.derive(code_hash, &config_desc, authority_hash, debuggable, hidden)
-}
-
-fn encode_tstr(tstr: &str, buffer: &mut Vec<u8>) -> Result<()> {
-    let bytes = tstr.as_bytes();
-    encode_header(3, bytes.len().try_into().unwrap(), buffer)?;
-    buffer.extend_from_slice(bytes);
-    Ok(())
-}
-
-fn encode_negative_number(n: i64, buffer: &mut dyn Write) -> Result<()> {
-    ensure!(n < 0);
-    let n = -1 - n;
-    encode_header(1, n.try_into().unwrap(), buffer)
+    dice.derive(code_hash, &config_descriptor, authority_hash, debuggable, hidden)
 }
 
 fn is_strict_boot() -> bool {
@@ -347,6 +308,13 @@
     !Path::new(DEBUG_MICRODROID_NO_VERIFIED_BOOT).exists()
 }
 
+fn should_export_tombstones(config: &VmPayloadConfig) -> bool {
+    match config.export_tombstones {
+        Some(b) => b,
+        None => system_properties::read_bool(DEBUGGABLE_PROP, true).unwrap_or(false),
+    }
+}
+
 fn try_run_payload(service: &Strong<dyn IVirtualMachineService>) -> Result<i32> {
     let metadata = load_metadata().context("Failed to load payload metadata")?;
     let dice = DiceDriver::new(Path::new("/dev/open-dice0")).context("Failed to load DICE")?;
@@ -406,12 +374,12 @@
 
     // To minimize the exposure to untrusted data, derive dice profile as soon as possible.
     info!("DICE derivation for payload");
-    let dice_context = dice_derivation(dice, &verified_data, &payload_metadata)?;
+    let dice_artifacts = dice_derivation(dice, &verified_data, &payload_metadata)?;
 
     // Run encryptedstore binary to prepare the storage
     let encryptedstore_child = if Path::new(ENCRYPTEDSTORE_BACKING_DEVICE).exists() {
         info!("Preparing encryptedstore ...");
-        Some(prepare_encryptedstore(&dice_context).context("encryptedstore run")?)
+        Some(prepare_encryptedstore(&dice_artifacts).context("encryptedstore run")?)
     } else {
         None
     };
@@ -456,7 +424,7 @@
     setup_config_sysprops(&config)?;
 
     // Start tombstone_transmit if enabled
-    if config.export_tombstones {
+    if should_export_tombstones(&config) {
         system_properties::write("tombstone_transmit.start", "1")
             .context("set tombstone_transmit.start")?;
     } else {
@@ -466,7 +434,7 @@
     // Wait until zipfuse has mounted the APKs so we can access the payload
     zipfuse.wait_until_done()?;
 
-    register_vm_payload_service(allow_restricted_apis, service.clone(), dice_context)?;
+    register_vm_payload_service(allow_restricted_apis, service.clone(), dice_artifacts)?;
 
     // Wait for encryptedstore to finish mounting the storage (if enabled) before setting
     // microdroid_manager.init_done. Reason is init stops uneventd after that.
@@ -481,7 +449,7 @@
         .context("set microdroid_manager.init_done")?;
 
     // Wait for tombstone_transmit to init
-    if config.export_tombstones {
+    if should_export_tombstones(&config) {
         wait_for_tombstone_transmit_done()?;
     }
 
@@ -712,6 +680,9 @@
     // Use the salt from a verified instance, or generate a salt for a new instance.
     let salt = if let Some(saved_data) = saved_data {
         saved_data.salt.clone()
+    } else if is_strict_boot() {
+        // No need to add more entropy as a previous stage must have used a new, random salt.
+        vec![0u8; 64]
     } else {
         let mut salt = vec![0u8; 64];
         salt.as_mut_slice().try_fill(&mut rand::thread_rng())?;
@@ -783,16 +754,23 @@
 }
 
 fn get_public_key_from_apk(apk: &str, root_hash_trustful: bool) -> Result<Box<[u8]>> {
+    let current_sdk = get_current_sdk()?;
     if !root_hash_trustful {
-        verify(apk).context(MicrodroidError::PayloadVerificationFailed(format!(
+        verify(apk, current_sdk).context(MicrodroidError::PayloadVerificationFailed(format!(
             "failed to verify {}",
             apk
         )))
     } else {
-        get_public_key_der(apk)
+        get_public_key_der(apk, current_sdk)
     }
 }
 
+fn get_current_sdk() -> Result<u32> {
+    let current_sdk = system_properties::read("ro.build.version.sdk")?;
+    let current_sdk = current_sdk.ok_or_else(|| anyhow!("SDK version missing"))?;
+    current_sdk.parse().context("Malformed SDK version")
+}
+
 fn load_config(payload_metadata: PayloadMetadata) -> Result<VmPayloadConfig> {
     match payload_metadata {
         PayloadMetadata::config_path(path) => {
@@ -813,7 +791,7 @@
                 apexes: vec![],
                 extra_apks: vec![],
                 prefer_staged: false,
-                export_tombstones: false,
+                export_tombstones: None,
                 enable_authfs: false,
             })
         }
@@ -900,7 +878,7 @@
     buf.iter().map(|b| format!("{:02X}", b)).collect()
 }
 
-fn prepare_encryptedstore(dice: &DiceContext) -> Result<Child> {
+fn prepare_encryptedstore(dice_artifacts: &OwnedDiceArtifacts) -> Result<Child> {
     // Use a fixed salt to scope the derivation to this API.
     // Generated using hexdump -vn32 -e'14/1 "0x%02X, " 1 "\n"' /dev/urandom
     // TODO(b/241541860) : Move this (& other salts) to a salt container, i.e. a global enum
@@ -909,11 +887,8 @@
         0x6F, 0xB3, 0xF9, 0x40, 0xCE, 0xDD, 0x99, 0x40, 0xAA, 0xA7, 0x0E, 0x92, 0x73, 0x90, 0x86,
         0x4A, 0x75,
     ];
-    let key = dice.get_sealing_key(
-        &salt,
-        ENCRYPTEDSTORE_KEY_IDENTIFIER.as_bytes(),
-        ENCRYPTEDSTORE_KEYSIZE,
-    )?;
+    let mut key = ZVec::new(ENCRYPTEDSTORE_KEYSIZE)?;
+    derive_sealing_key(dice_artifacts, &salt, ENCRYPTEDSTORE_KEY_IDENTIFIER.as_bytes(), &mut key)?;
 
     let mut cmd = Command::new(ENCRYPTEDSTORE_BIN);
     cmd.arg("--blkdevice")
diff --git a/microdroid_manager/src/vm_payload_service.rs b/microdroid_manager/src/vm_payload_service.rs
index 98b9f2b..96f51f0 100644
--- a/microdroid_manager/src/vm_payload_service.rs
+++ b/microdroid_manager/src/vm_payload_service.rs
@@ -14,22 +14,21 @@
 
 //! Implementation of the AIDL interface `IVmPayloadService`.
 
-use crate::dice::DiceContext;
+use crate::dice::derive_sealing_key;
 use android_system_virtualization_payload::aidl::android::system::virtualization::payload::IVmPayloadService::{
     BnVmPayloadService, IVmPayloadService, VM_PAYLOAD_SERVICE_SOCKET_NAME};
 use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice::IVirtualMachineService::IVirtualMachineService;
 use anyhow::Result;
 use binder::{Interface, BinderFeatures, ExceptionCode, Status, Strong};
+use diced_open_dice::{DiceArtifacts, OwnedDiceArtifacts};
 use log::{error, info};
-use openssl::hkdf::hkdf;
-use openssl::md::Md;
 use rpcbinder::RpcServer;
 
 /// Implementation of `IVmPayloadService`.
 struct VmPayloadService {
     allow_restricted_apis: bool,
     virtual_machine_service: Strong<dyn IVirtualMachineService>,
-    dice: DiceContext,
+    dice: OwnedDiceArtifacts,
 }
 
 impl IVmPayloadService for VmPayloadService {
@@ -48,7 +47,7 @@
             0xB7, 0xA8, 0x43, 0x92,
         ];
         let mut secret = vec![0; size.try_into().unwrap()];
-        hkdf(&mut secret, Md::sha256(), &self.dice.cdi_seal, &salt, identifier).map_err(|e| {
+        derive_sealing_key(&self.dice, &salt, identifier, &mut secret).map_err(|e| {
             error!("Failed to derive VM instance secret: {:?}", e);
             Status::new_service_specific_error(-1, None)
         })?;
@@ -57,12 +56,16 @@
 
     fn getDiceAttestationChain(&self) -> binder::Result<Vec<u8>> {
         self.check_restricted_apis_allowed()?;
-        Ok(self.dice.bcc.clone())
+        if let Some(bcc) = self.dice.bcc() {
+            Ok(bcc.to_vec())
+        } else {
+            Err(Status::new_exception_str(ExceptionCode::ILLEGAL_STATE, Some("bcc is none")))
+        }
     }
 
     fn getDiceAttestationCdi(&self) -> binder::Result<Vec<u8>> {
         self.check_restricted_apis_allowed()?;
-        Ok(self.dice.cdi_attest.to_vec())
+        Ok(self.dice.cdi_attest().to_vec())
     }
 }
 
@@ -73,7 +76,7 @@
     fn new(
         allow_restricted_apis: bool,
         vm_service: Strong<dyn IVirtualMachineService>,
-        dice: DiceContext,
+        dice: OwnedDiceArtifacts,
     ) -> Self {
         Self { allow_restricted_apis, virtual_machine_service: vm_service, dice }
     }
@@ -92,7 +95,7 @@
 pub(crate) fn register_vm_payload_service(
     allow_restricted_apis: bool,
     vm_service: Strong<dyn IVirtualMachineService>,
-    dice: DiceContext,
+    dice: OwnedDiceArtifacts,
 ) -> Result<()> {
     let vm_payload_binder = BnVmPayloadService::new_binder(
         VmPayloadService::new(allow_restricted_apis, vm_service, dice),
diff --git a/pvmfw/Android.bp b/pvmfw/Android.bp
index 21f84a5..0d845f9 100644
--- a/pvmfw/Android.bp
+++ b/pvmfw/Android.bp
@@ -13,15 +13,19 @@
     ],
     rustlibs: [
         "libaarch64_paging",
+        "libbssl_ffi_nostd",
         "libbuddy_system_allocator",
-        "libdice_nostd",
+        "libdiced_open_dice_nostd",
         "libfdtpci",
         "liblibfdt",
         "liblog_rust_nostd",
         "libonce_cell_nostd",
         "libpvmfw_avb_nostd",
         "libpvmfw_embedded_key",
+        "libpvmfw_fdt_template",
+        "libstatic_assertions",
         "libtinyvec_nostd",
+        "libuuid_nostd",
         "libvirtio_drivers",
         "libvmbase",
         "libzeroize_nostd",
@@ -50,6 +54,7 @@
     // partition image. This is just to package the unstripped file into the
     // symbols zip file for debugging purpose.
     installable: true,
+    native_coverage: false,
 }
 
 raw_binary {
@@ -64,6 +69,22 @@
     },
 }
 
+// Provide pvmfw.bin binary regardless of the architecture for building test.
+// Note that skipping tests on unsupported device is easy
+// while configuring server configuration to make such tests to run on working
+// devices.
+prebuilt_etc {
+    name: "pvmfw_test",
+    filename: "pvmfw_test.bin",
+    target: {
+        android_arm64: {
+            src: ":pvmfw_bin",
+        },
+    },
+    src: "empty_file",
+    installable: false,
+}
+
 prebuilt_etc {
     name: "pvmfw_embedded_key",
     src: ":avb_testkey_rsa4096_pub_bin",
@@ -98,6 +119,37 @@
     installable: false,
 }
 
+// platform.dts is passed to clang for macro preprocessing, and then compiled to dtbo using dtc.
+// The raw content of the dtbo file is then written as a Rust byte array.
+genrule {
+    name: "pvmfw_fdt_template_rs",
+    srcs: [
+        "platform.dts",
+        ":arm_dt_bindings_headers", // implicit dependency
+    ],
+    out: ["lib.rs"],
+    tools: ["dtc"],
+    cmd: "prebuilts/clang/host/linux-x86/clang-r487747/bin/clang " + // UGLY!!!
+        "-E -P -x assembler-with-cpp -I external/arm-trusted-firmware/include " +
+        "-o $(genDir)/preprocessed.dts $(location platform.dts) && " +
+        "$(location dtc) -I dts -O dtb -o $(genDir)/compiled.dtbo $(genDir)/preprocessed.dts && " +
+        "(" +
+        "    echo '#![no_std]';" +
+        "    echo '#![allow(missing_docs)]';" +
+        "    echo 'pub const RAW: &[u8] = &[';" +
+        "    xxd -i < $(genDir)/compiled.dtbo;" +
+        "    echo '];';" +
+        ") > $(out)",
+}
+
+rust_library_rlib {
+    name: "libpvmfw_fdt_template",
+    defaults: ["vmbase_ffi_defaults"],
+    prefer_rlib: true,
+    srcs: [":pvmfw_fdt_template_rs"],
+    crate_name: "pvmfw_fdt_template",
+}
+
 bootimg {
     name: "pvmfw_img",
     stem: "pvmfw.img",
diff --git a/pvmfw/README.md b/pvmfw/README.md
index 1e4b605..c652f01 100644
--- a/pvmfw/README.md
+++ b/pvmfw/README.md
@@ -61,13 +61,27 @@
 
 Starting in Android T, the `PRODUCT_BUILD_PVMFW_IMAGE` build variable controls
 the generation of `pvmfw.img`, a new [ABL partition][ABL-part] containing the
-pvmfw binary and following the internal format of the [`boot`][boot-img]
-partition, intended to be verified and loaded by ABL on AVF-compatible devices.
+pvmfw binary (sometimes called "`pvmfw.bin`") and following the internal format
+of the [`boot`][boot-img] partition, intended to be verified and loaded by ABL
+on AVF-compatible devices.
+
+Once ABL has verified the `pvmfw.img` chained static partition, the contained
+[`boot.img` header][boot-img] may be used to obtain the size of the `pvmfw.bin`
+image (recorded in the `kernel_size` field), as it already does for the kernel
+itself. In accordance with the header format, the `kernel_size` bytes of the
+partition following the header will be the `pvmfw.bin` image.
+
+Note that when it gets executed in the context of a pVM, `pvmfw` expects to have
+been loaded at 4KiB-aligned intermediate physical address (IPA) so if ABL loads
+the `pvmfw.bin` image without respecting this alignment, it is the
+responsibility of the hypervisor to either reject the image or copy it into
+guest address space with the right alignment.
 
 To support pKVM, ABL is expected to describe the region using a reserved memory
 device tree node where both address and size have been properly aligned to the
-page size used by the hypervisor. For example, the following node describes a
-region of size `0x40000` at address `0x80000000`:
+page size used by the hypervisor. This single region must include both the pvmfw
+binary image and its configuration data (see below). For example, the following
+node describes a region of size `0x40000` at address `0x80000000`:
 ```
 reserved-memory {
     ...
diff --git a/pvmfw/avb/Android.bp b/pvmfw/avb/Android.bp
index 0c1e392..7ed4895 100644
--- a/pvmfw/avb/Android.bp
+++ b/pvmfw/avb/Android.bp
@@ -8,11 +8,11 @@
     srcs: ["src/lib.rs"],
     prefer_rlib: true,
     rustlibs: [
-        "libavb_bindgen",
+        "libavb_bindgen_nostd",
         "libtinyvec_nostd",
     ],
     whole_static_libs: [
-        "libavb_non_debug",
+        "libavb_baremetal",
     ],
 }
 
diff --git a/pvmfw/avb/src/lib.rs b/pvmfw/avb/src/lib.rs
index 8fea162..d83737f 100644
--- a/pvmfw/avb/src/lib.rs
+++ b/pvmfw/avb/src/lib.rs
@@ -15,8 +15,6 @@
 //! A library implementing the payload verification for pvmfw with libavb
 
 #![cfg_attr(not(test), no_std)]
-// For usize.checked_add_signed(isize), available in Rust 1.66.0
-#![feature(mixed_integer_ops)]
 
 mod descriptor;
 mod error;
diff --git a/pvmfw/avb/src/verify.rs b/pvmfw/avb/src/verify.rs
index 1a79c83..b03506c 100644
--- a/pvmfw/avb/src/verify.rs
+++ b/pvmfw/avb/src/verify.rs
@@ -22,14 +22,16 @@
 use core::ffi::c_char;
 
 /// Verified data returned when the payload verification succeeds.
-#[derive(Debug)]
-pub struct VerifiedBootData {
+#[derive(Debug, PartialEq, Eq)]
+pub struct VerifiedBootData<'a> {
     /// DebugLevel of the VM.
     pub debug_level: DebugLevel,
     /// Kernel digest.
     pub kernel_digest: Digest,
     /// Initrd digest if initrd exists.
     pub initrd_digest: Option<Digest>,
+    /// Trusted public key.
+    pub public_key: &'a [u8],
 }
 
 /// This enum corresponds to the `DebugLevel` in `VirtualMachineConfig`.
@@ -94,11 +96,11 @@
 }
 
 /// Verifies the payload (signed kernel + initrd) against the trusted public key.
-pub fn verify_payload(
+pub fn verify_payload<'a>(
     kernel: &[u8],
     initrd: Option<&[u8]>,
-    trusted_public_key: &[u8],
-) -> Result<VerifiedBootData, AvbSlotVerifyError> {
+    trusted_public_key: &'a [u8],
+) -> Result<VerifiedBootData<'a>, AvbSlotVerifyError> {
     let mut payload = Payload::new(kernel, initrd, trusted_public_key);
     let mut ops = Ops::from(&mut payload);
     let kernel_verify_result = ops.verify_partition(PartitionName::Kernel.as_cstr())?;
@@ -119,6 +121,7 @@
             debug_level: DebugLevel::None,
             kernel_digest: kernel_descriptor.digest,
             initrd_digest: None,
+            public_key: trusted_public_key,
         });
     }
 
@@ -142,5 +145,6 @@
         debug_level,
         kernel_digest: kernel_descriptor.digest,
         initrd_digest: Some(initrd_descriptor.digest),
+        public_key: trusted_public_key,
     })
 }
diff --git a/pvmfw/avb/tests/api_test.rs b/pvmfw/avb/tests/api_test.rs
index 261d8a8..78f274a 100644
--- a/pvmfw/avb/tests/api_test.rs
+++ b/pvmfw/avb/tests/api_test.rs
@@ -18,7 +18,7 @@
 
 use anyhow::{anyhow, Result};
 use avb_bindgen::{AvbFooter, AvbVBMetaImageHeader};
-use pvmfw_avb::{verify_payload, AvbSlotVerifyError, DebugLevel};
+use pvmfw_avb::{verify_payload, AvbSlotVerifyError, DebugLevel, VerifiedBootData};
 use std::{fs, mem::size_of, ptr};
 use utils::*;
 
@@ -53,17 +53,23 @@
 
 #[test]
 fn payload_expecting_no_initrd_passes_verification_with_no_initrd() -> Result<()> {
+    let public_key = load_trusted_public_key()?;
     let verified_boot_data = verify_payload(
         &fs::read(TEST_IMG_WITH_ONE_HASHDESC_PATH)?,
         /*initrd=*/ None,
-        &load_trusted_public_key()?,
+        &public_key,
     )
     .map_err(|e| anyhow!("Verification failed. Error: {}", e))?;
 
-    assert_eq!(DebugLevel::None, verified_boot_data.debug_level);
-    let digest = hash(&[&hex::decode("1111")?, &fs::read(UNSIGNED_TEST_IMG_PATH)?]);
-    assert_eq!(digest, verified_boot_data.kernel_digest);
-    assert!(verified_boot_data.initrd_digest.is_none());
+    let kernel_digest = hash(&[&hex::decode("1111")?, &fs::read(UNSIGNED_TEST_IMG_PATH)?]);
+    let expected_boot_data = VerifiedBootData {
+        debug_level: DebugLevel::None,
+        kernel_digest,
+        initrd_digest: None,
+        public_key: &public_key,
+    };
+    assert_eq!(expected_boot_data, verified_boot_data);
+
     Ok(())
 }
 
diff --git a/pvmfw/avb/tests/utils.rs b/pvmfw/avb/tests/utils.rs
index 8756d06..6713846 100644
--- a/pvmfw/avb/tests/utils.rs
+++ b/pvmfw/avb/tests/utils.rs
@@ -22,7 +22,7 @@
     AvbVBMetaImageHeader,
 };
 use openssl::sha;
-use pvmfw_avb::{verify_payload, AvbSlotVerifyError, DebugLevel, Digest};
+use pvmfw_avb::{verify_payload, AvbSlotVerifyError, DebugLevel, Digest, VerifiedBootData};
 use std::{
     fs,
     mem::{size_of, transmute, MaybeUninit},
@@ -102,23 +102,23 @@
     initrd_salt: &[u8],
     expected_debug_level: DebugLevel,
 ) -> Result<()> {
+    let public_key = load_trusted_public_key()?;
     let kernel = load_latest_signed_kernel()?;
-    let verified_boot_data = verify_payload(&kernel, Some(initrd), &load_trusted_public_key()?)
+    let verified_boot_data = verify_payload(&kernel, Some(initrd), &public_key)
         .map_err(|e| anyhow!("Verification failed. Error: {}", e))?;
 
-    assert_eq!(expected_debug_level, verified_boot_data.debug_level);
-
     let footer = extract_avb_footer(&kernel)?;
-    assert_eq!(
-        hash(&[&hash(&[b"bootloader"]), &kernel[..usize::try_from(footer.original_image_size)?]]),
-        verified_boot_data.kernel_digest,
-        "Kernel digest is not equal to the expected."
-    );
-    assert_eq!(
-        hash(&[&hash(&[initrd_salt]), initrd,]),
-        verified_boot_data.initrd_digest.unwrap(),
-        "initrd digest is not equal to the expected."
-    );
+    let kernel_digest =
+        hash(&[&hash(&[b"bootloader"]), &kernel[..usize::try_from(footer.original_image_size)?]]);
+    let initrd_digest = Some(hash(&[&hash(&[initrd_salt]), initrd]));
+    let expected_boot_data = VerifiedBootData {
+        debug_level: expected_debug_level,
+        kernel_digest,
+        initrd_digest,
+        public_key: &public_key,
+    };
+    assert_eq!(expected_boot_data, verified_boot_data);
+
     Ok(())
 }
 
diff --git a/pvmfw/empty_file b/pvmfw/empty_file
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pvmfw/empty_file
diff --git a/pvmfw/platform.dts b/pvmfw/platform.dts
new file mode 100644
index 0000000..127f69a
--- /dev/null
+++ b/pvmfw/platform.dts
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2022 Google LLC
+ */
+
+#include <dt-bindings/interrupt-controller/arm-gic.h>
+
+#define PLACEHOLDER	0xffffffff
+#define PLACEHOLDER2	PLACEHOLDER PLACEHOLDER
+#define PLACEHOLDER4	PLACEHOLDER2 PLACEHOLDER2
+
+/dts-v1/;
+
+/ {
+	interrupt-parent = <&intc>;
+	compatible = "linux,dummy-virt";
+	#address-cells = <2>;
+	#size-cells = <2>;
+
+	chosen {
+		stdout-path = "/uart@3f8";
+		linux,pci-probe-only = <1>;
+		kaslr-seed = <PLACEHOLDER2>;
+		avf,strict-boot;
+		avf,new-instance;
+	};
+
+	memory {
+		device_type = "memory";
+		reg = <0x00 0x80000000 PLACEHOLDER2>;
+	};
+
+	reserved-memory {
+		#address-cells = <2>;
+		#size-cells = <2>;
+		ranges;
+		swiotlb: restricted_dma_reserved {
+			compatible = "restricted-dma-pool";
+			size = <PLACEHOLDER2>;
+			alignment = <PLACEHOLDER2>;
+		};
+
+		dice {
+			compatible = "google,open-dice";
+			no-map;
+			reg = <PLACEHOLDER4>;
+		};
+	};
+
+	cpus {
+		#address-cells = <1>;
+		#size-cells = <0>;
+		cpu@0 {
+			device_type = "cpu";
+			compatible = "arm,arm-v8";
+			enable-method = "psci";
+			reg = <0>;
+		};
+		cpu@1 {
+			device_type = "cpu";
+			compatible = "arm,arm-v8";
+			enable-method = "psci";
+			reg = <1>;
+		};
+		cpu@2 {
+			device_type = "cpu";
+			compatible = "arm,arm-v8";
+			enable-method = "psci";
+			reg = <2>;
+		};
+		cpu@3 {
+			device_type = "cpu";
+			compatible = "arm,arm-v8";
+			enable-method = "psci";
+			reg = <3>;
+		};
+		cpu@4 {
+			device_type = "cpu";
+			compatible = "arm,arm-v8";
+			enable-method = "psci";
+			reg = <4>;
+		};
+		cpu@5 {
+			device_type = "cpu";
+			compatible = "arm,arm-v8";
+			enable-method = "psci";
+			reg = <5>;
+		};
+		cpu@6 {
+			device_type = "cpu";
+			compatible = "arm,arm-v8";
+			enable-method = "psci";
+			reg = <6>;
+		};
+		cpu@7 {
+			device_type = "cpu";
+			compatible = "arm,arm-v8";
+			enable-method = "psci";
+			reg = <7>;
+		};
+		cpu@8 {
+			device_type = "cpu";
+			compatible = "arm,arm-v8";
+			enable-method = "psci";
+			reg = <8>;
+		};
+		cpu@9 {
+			device_type = "cpu";
+			compatible = "arm,arm-v8";
+			enable-method = "psci";
+			reg = <9>;
+		};
+		cpu@10 {
+			device_type = "cpu";
+			compatible = "arm,arm-v8";
+			enable-method = "psci";
+			reg = <10>;
+		};
+		cpu@11 {
+			device_type = "cpu";
+			compatible = "arm,arm-v8";
+			enable-method = "psci";
+			reg = <11>;
+		};
+		cpu@12 {
+			device_type = "cpu";
+			compatible = "arm,arm-v8";
+			enable-method = "psci";
+			reg = <12>;
+		};
+		cpu@13 {
+			device_type = "cpu";
+			compatible = "arm,arm-v8";
+			enable-method = "psci";
+			reg = <13>;
+		};
+		cpu@14 {
+			device_type = "cpu";
+			compatible = "arm,arm-v8";
+			enable-method = "psci";
+			reg = <14>;
+		};
+		cpu@15 {
+			device_type = "cpu";
+			compatible = "arm,arm-v8";
+			enable-method = "psci";
+			reg = <15>;
+		};
+	};
+
+	intc: intc {
+		compatible = "arm,gic-v3";
+		#address-cells = <2>;
+		#size-cells = <2>;
+		#interrupt-cells = <3>;
+		interrupt-controller;
+		reg = <0x00 0x3fff0000 0x00 0x10000>, <PLACEHOLDER4>;
+	};
+
+	timer {
+		compatible = "arm,armv8-timer";
+		always-on;
+		/* The IRQ type needs to be OR-ed with the CPU mask */
+		interrupts = <GIC_PPI 0xd IRQ_TYPE_LEVEL_LOW
+		              GIC_PPI 0xe IRQ_TYPE_LEVEL_LOW
+			      GIC_PPI 0xb IRQ_TYPE_LEVEL_LOW
+			      GIC_PPI 0xa IRQ_TYPE_LEVEL_LOW>;
+	};
+
+	uart@2e8 {
+		compatible = "ns16550a";
+		reg = <0x00 0x2e8 0x00 0x8>;
+		clock-frequency = <0x1c2000>;
+		interrupts = <GIC_SPI 2 IRQ_TYPE_EDGE_RISING>;
+	};
+
+	uart@2f8 {
+		compatible = "ns16550a";
+		reg = <0x00 0x2f8 0x00 0x8>;
+		clock-frequency = <0x1c2000>;
+		interrupts = <GIC_SPI 2 IRQ_TYPE_EDGE_RISING>;
+	};
+
+	uart@3e8 {
+		compatible = "ns16550a";
+		reg = <0x00 0x3e8 0x00 0x8>;
+		clock-frequency = <0x1c2000>;
+		interrupts = <GIC_SPI 0 IRQ_TYPE_EDGE_RISING>;
+	};
+
+	uart@3f8 {
+		compatible = "ns16550a";
+		reg = <0x00 0x3f8 0x00 0x8>;
+		clock-frequency = <0x1c2000>;
+		interrupts = <GIC_SPI 0 IRQ_TYPE_EDGE_RISING>;
+	};
+
+	psci {
+		compatible = "arm,psci-1.0";
+		method = "hvc";
+	};
+
+	pci {
+		compatible = "pci-host-cam-generic";
+		device_type = "pci";
+		#address-cells = <3>;
+		#size-cells = <2>;
+		#interrupt-cells = <1>;
+		dma-coherent;
+		memory-region = <&swiotlb>;
+		ranges = <
+			0x3000000 0x0 0x02000000 0x0 0x02000000 0x00 0x02000000
+			0x3000000 PLACEHOLDER2   PLACEHOLDER2   PLACEHOLDER2
+		>;
+		bus-range = <0x00 0x00>;
+		reg = <0x00 0x10000 0x00 0x1000000>;
+		interrupt-map = <
+			0x0800 0x0 0x0 1 &intc 0 0 GIC_SPI 3 IRQ_TYPE_LEVEL_HIGH
+			0x1000 0x0 0x0 1 &intc 0 0 GIC_SPI 4 IRQ_TYPE_LEVEL_HIGH
+			0x1800 0x0 0x0 1 &intc 0 0 GIC_SPI 5 IRQ_TYPE_LEVEL_HIGH
+			0x2000 0x0 0x0 1 &intc 0 0 GIC_SPI 6 IRQ_TYPE_LEVEL_HIGH
+			0x2800 0x0 0x0 1 &intc 0 0 GIC_SPI 7 IRQ_TYPE_LEVEL_HIGH
+			0x3000 0x0 0x0 1 &intc 0 0 GIC_SPI 8 IRQ_TYPE_LEVEL_HIGH
+			0x3800 0x0 0x0 1 &intc 0 0 GIC_SPI 9 IRQ_TYPE_LEVEL_HIGH
+		>;
+		interrupt-map-mask = <0xf800 0x0 0x0 0x7
+				      0xf800 0x0 0x0 0x7
+				      0xf800 0x0 0x0 0x7
+				      0xf800 0x0 0x0 0x7
+				      0xf800 0x0 0x0 0x7
+				      0xf800 0x0 0x0 0x7
+				      0xf800 0x0 0x0 0x7>;
+	};
+
+	clk: pclk@3M {
+		compatible = "fixed-clock";
+		clock-frequency = <0x2fefd8>;
+		#clock-cells = <0>;
+	};
+
+	rtc@2000 {
+		compatible = "arm,primecell";
+		arm,primecell-periphid = <0x41030>;
+		reg = <0x00 0x2000 0x00 0x1000>;
+		interrupts = <GIC_SPI 1 IRQ_TYPE_LEVEL_HIGH>;
+		clock-names = "apb_pclk";
+		clocks = <&clk>;
+	};
+
+	vmwdt@3000 {
+		compatible = "qemu,vcpu-stall-detector";
+		reg = <0x00 0x3000 0x00 0x1000>;
+		clock-frequency = <10>;
+		timeout-sec = <8>;
+	};
+};
diff --git a/pvmfw/src/config.rs b/pvmfw/src/config.rs
index f209784..f62a580 100644
--- a/pvmfw/src/config.rs
+++ b/pvmfw/src/config.rs
@@ -216,12 +216,22 @@
     }
 
     /// Get slice containing the platform BCC.
-    pub fn get_bcc_mut(&mut self) -> &mut [u8] {
-        &mut self.body[self.bcc_range.clone()]
-    }
+    pub fn get_entries(&mut self) -> (&mut [u8], Option<&mut [u8]>) {
+        let bcc_start = self.bcc_range.start;
+        let bcc_end = self.bcc_range.len();
+        let (_, rest) = self.body.split_at_mut(bcc_start);
+        let (bcc, rest) = rest.split_at_mut(bcc_end);
 
-    /// Get slice containing the platform debug policy.
-    pub fn get_debug_policy(&mut self) -> Option<&mut [u8]> {
-        self.dp_range.as_ref().map(|r| &mut self.body[r.clone()])
+        let dp = if let Some(dp_range) = &self.dp_range {
+            let dp_start = dp_range.start.checked_sub(self.bcc_range.end).unwrap();
+            let dp_end = dp_range.len();
+            let (_, rest) = rest.split_at_mut(dp_start);
+            let (dp, _) = rest.split_at_mut(dp_end);
+            Some(dp)
+        } else {
+            None
+        };
+
+        (bcc, dp)
     }
 }
diff --git a/pvmfw/src/crypto.rs b/pvmfw/src/crypto.rs
new file mode 100644
index 0000000..85dc6c9
--- /dev/null
+++ b/pvmfw/src/crypto.rs
@@ -0,0 +1,302 @@
+// Copyright 2023, 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.
+
+//! Wrapper around BoringSSL/OpenSSL symbols.
+
+use core::convert::AsRef;
+use core::ffi::{c_char, c_int, CStr};
+use core::fmt;
+use core::mem::MaybeUninit;
+use core::num::NonZeroU32;
+use core::ptr;
+
+use bssl_ffi::ERR_get_error_line;
+use bssl_ffi::ERR_lib_error_string;
+use bssl_ffi::ERR_reason_error_string;
+use bssl_ffi::EVP_AEAD_CTX_aead;
+use bssl_ffi::EVP_AEAD_CTX_init;
+use bssl_ffi::EVP_AEAD_CTX_open;
+use bssl_ffi::EVP_AEAD_CTX_seal;
+use bssl_ffi::EVP_AEAD_max_overhead;
+use bssl_ffi::EVP_aead_aes_256_gcm_randnonce;
+use bssl_ffi::EVP_sha512;
+use bssl_ffi::EVP_AEAD;
+use bssl_ffi::EVP_AEAD_CTX;
+use bssl_ffi::HKDF;
+
+#[derive(Debug)]
+pub struct Error {
+    packed: NonZeroU32,
+    file: Option<&'static CStr>,
+    line: c_int,
+}
+
+impl Error {
+    fn get() -> Option<Self> {
+        let mut file = MaybeUninit::uninit();
+        let mut line = MaybeUninit::uninit();
+        // SAFETY - The function writes to the provided pointers, validated below.
+        let packed = unsafe { ERR_get_error_line(file.as_mut_ptr(), line.as_mut_ptr()) };
+        // SAFETY - Any possible value returned could be considered a valid *const c_char.
+        let file = unsafe { file.assume_init() };
+        // SAFETY - Any possible value returned could be considered a valid c_int.
+        let line = unsafe { line.assume_init() };
+
+        let packed = packed.try_into().ok()?;
+        // SAFETY - Any non-NULL result is expected to point to a global const C string.
+        let file = unsafe { as_static_cstr(file) };
+
+        Some(Self { packed, file, line })
+    }
+
+    fn packed_value(&self) -> u32 {
+        self.packed.get()
+    }
+
+    fn library_name(&self) -> Option<&'static CStr> {
+        // SAFETY - Call to a pure function.
+        let name = unsafe { ERR_lib_error_string(self.packed_value()) };
+        // SAFETY - Any non-NULL result is expected to point to a global const C string.
+        unsafe { as_static_cstr(name) }
+    }
+
+    fn reason(&self) -> Option<&'static CStr> {
+        // SAFETY - Call to a pure function.
+        let reason = unsafe { ERR_reason_error_string(self.packed_value()) };
+        // SAFETY - Any non-NULL result is expected to point to a global const C string.
+        unsafe { as_static_cstr(reason) }
+    }
+}
+
+impl fmt::Display for Error {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        let unknown_library = CStr::from_bytes_with_nul(b"{unknown library}\0").unwrap();
+        let unknown_reason = CStr::from_bytes_with_nul(b"{unknown reason}\0").unwrap();
+        let unknown_file = CStr::from_bytes_with_nul(b"??\0").unwrap();
+
+        let packed = self.packed_value();
+        let library = self.library_name().unwrap_or(unknown_library).to_str().unwrap();
+        let reason = self.reason().unwrap_or(unknown_reason).to_str().unwrap();
+        let file = self.file.unwrap_or(unknown_file).to_str().unwrap();
+        let line = self.line;
+
+        write!(f, "{file}:{line}: {library}: {reason} ({packed:#x})")
+    }
+}
+
+#[derive(Copy, Clone)]
+pub struct ErrorIterator {}
+
+impl Iterator for ErrorIterator {
+    type Item = Error;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        Self::Item::get()
+    }
+}
+
+pub type Result<T> = core::result::Result<T, ErrorIterator>;
+
+#[repr(transparent)]
+pub struct Aead(EVP_AEAD);
+
+impl Aead {
+    pub fn aes_256_gcm_randnonce() -> Option<&'static Self> {
+        // SAFETY - Returned pointer is checked below.
+        let aead = unsafe { EVP_aead_aes_256_gcm_randnonce() };
+        if aead.is_null() {
+            None
+        } else {
+            // SAFETY - We assume that the non-NULL value points to a valid and static EVP_AEAD.
+            Some(unsafe { &*(aead as *const _) })
+        }
+    }
+
+    pub fn max_overhead(&self) -> usize {
+        // SAFETY - Function should only read from self.
+        unsafe { EVP_AEAD_max_overhead(self.as_ref() as *const _) }
+    }
+}
+
+#[repr(transparent)]
+pub struct AeadCtx(EVP_AEAD_CTX);
+
+impl AeadCtx {
+    pub fn new_aes_256_gcm_randnonce(key: &[u8]) -> Result<Self> {
+        let aead = Aead::aes_256_gcm_randnonce().unwrap();
+
+        Self::new(aead, key)
+    }
+
+    fn new(aead: &'static Aead, key: &[u8]) -> Result<Self> {
+        const DEFAULT_TAG_LENGTH: usize = 0;
+        let engine = ptr::null_mut(); // Use default implementation.
+        let mut ctx = MaybeUninit::zeroed();
+        // SAFETY - Initialize the EVP_AEAD_CTX with const pointers to the AEAD and key.
+        let result = unsafe {
+            EVP_AEAD_CTX_init(
+                ctx.as_mut_ptr(),
+                aead.as_ref() as *const _,
+                key.as_ptr(),
+                key.len(),
+                DEFAULT_TAG_LENGTH,
+                engine,
+            )
+        };
+
+        if result == 1 {
+            // SAFETY - We assume that the non-NULL value points to a valid and static EVP_AEAD.
+            Ok(Self(unsafe { ctx.assume_init() }))
+        } else {
+            Err(ErrorIterator {})
+        }
+    }
+
+    pub fn aead(&self) -> Option<&'static Aead> {
+        // SAFETY - The function should only read from self.
+        let aead = unsafe { EVP_AEAD_CTX_aead(self.as_ref() as *const _) };
+        if aead.is_null() {
+            None
+        } else {
+            // SAFETY - We assume that the non-NULL value points to a valid and static EVP_AEAD.
+            Some(unsafe { &*(aead as *const _) })
+        }
+    }
+
+    pub fn open<'b>(&self, out: &'b mut [u8], data: &[u8]) -> Result<&'b mut [u8]> {
+        let nonce = ptr::null_mut();
+        let nonce_len = 0;
+        let ad = ptr::null_mut();
+        let ad_len = 0;
+        let mut out_len = MaybeUninit::uninit();
+        // SAFETY - The function should only read from self and write to out (at most the provided
+        // number of bytes) and out_len while reading from data (at most the provided number of
+        // bytes), ignoring any NULL input.
+        let result = unsafe {
+            EVP_AEAD_CTX_open(
+                self.as_ref() as *const _,
+                out.as_mut_ptr(),
+                out_len.as_mut_ptr(),
+                out.len(),
+                nonce,
+                nonce_len,
+                data.as_ptr(),
+                data.len(),
+                ad,
+                ad_len,
+            )
+        };
+
+        if result == 1 {
+            // SAFETY - Any value written to out_len could be a valid usize. The value itself is
+            // validated as being a proper slice length by panicking in the following indexing
+            // otherwise.
+            let out_len = unsafe { out_len.assume_init() };
+            Ok(&mut out[..out_len])
+        } else {
+            Err(ErrorIterator {})
+        }
+    }
+
+    pub fn seal<'b>(&self, out: &'b mut [u8], data: &[u8]) -> Result<&'b mut [u8]> {
+        let nonce = ptr::null_mut();
+        let nonce_len = 0;
+        let ad = ptr::null_mut();
+        let ad_len = 0;
+        let mut out_len = MaybeUninit::uninit();
+        // SAFETY - The function should only read from self and write to out (at most the provided
+        // number of bytes) while reading from data (at most the provided number of bytes),
+        // ignoring any NULL input.
+        let result = unsafe {
+            EVP_AEAD_CTX_seal(
+                self.as_ref() as *const _,
+                out.as_mut_ptr(),
+                out_len.as_mut_ptr(),
+                out.len(),
+                nonce,
+                nonce_len,
+                data.as_ptr(),
+                data.len(),
+                ad,
+                ad_len,
+            )
+        };
+
+        if result == 1 {
+            // SAFETY - Any value written to out_len could be a valid usize. The value itself is
+            // validated as being a proper slice length by panicking in the following indexing
+            // otherwise.
+            let out_len = unsafe { out_len.assume_init() };
+            Ok(&mut out[..out_len])
+        } else {
+            Err(ErrorIterator {})
+        }
+    }
+}
+
+/// Cast a C string pointer to a static non-mutable reference.
+///
+/// # Safety
+///
+/// The caller needs to ensure that the pointer points to a valid C string and that the C lifetime
+/// of the string is compatible with a static Rust lifetime.
+unsafe fn as_static_cstr(p: *const c_char) -> Option<&'static CStr> {
+    if p.is_null() {
+        None
+    } else {
+        Some(CStr::from_ptr(p))
+    }
+}
+
+impl AsRef<EVP_AEAD> for Aead {
+    fn as_ref(&self) -> &EVP_AEAD {
+        &self.0
+    }
+}
+
+impl AsRef<EVP_AEAD_CTX> for AeadCtx {
+    fn as_ref(&self) -> &EVP_AEAD_CTX {
+        &self.0
+    }
+}
+
+pub fn hkdf_sh512<const N: usize>(secret: &[u8], salt: &[u8], info: &[u8]) -> Result<[u8; N]> {
+    let mut key = [0; N];
+    // SAFETY - The function shouldn't access any Rust variable and the returned value is accepted
+    // as a potentially NULL pointer.
+    let digest = unsafe { EVP_sha512() };
+
+    assert!(!digest.is_null());
+    // SAFETY - Only reads from/writes to the provided slices and supports digest was checked not
+    // be NULL.
+    let result = unsafe {
+        HKDF(
+            key.as_mut_ptr(),
+            key.len(),
+            digest,
+            secret.as_ptr(),
+            secret.len(),
+            salt.as_ptr(),
+            salt.len(),
+            info.as_ptr(),
+            info.len(),
+        )
+    };
+
+    if result == 1 {
+        Ok(key)
+    } else {
+        Err(ErrorIterator {})
+    }
+}
diff --git a/pvmfw/src/debug_policy.rs b/pvmfw/src/debug_policy.rs
index 37e2af8..23d3e1d 100644
--- a/pvmfw/src/debug_policy.rs
+++ b/pvmfw/src/debug_policy.rs
@@ -14,7 +14,7 @@
 
 //! Support for the debug policy overlay in pvmfw
 
-use alloc::vec;
+use alloc::{vec, vec::Vec};
 use core::ffi::CStr;
 use core::fmt;
 use libfdt::FdtError;
@@ -63,12 +63,8 @@
     fdt.pack().map_err(|e| DebugPolicyError::OverlaidFdt("Failed to re-pack", e))
 }
 
-/// Dsiables ramdump by removing crashkernel from bootargs in /chosen.
-///
-/// # Safety
-///
-/// This may corrupt the input `Fdt` when error happens while editing prop value.
-unsafe fn disable_ramdump(fdt: &mut libfdt::Fdt) -> Result<(), DebugPolicyError> {
+/// Disables ramdump by removing crashkernel from bootargs in /chosen.
+fn disable_ramdump(fdt: &mut libfdt::Fdt) -> Result<(), DebugPolicyError> {
     let chosen_path = CStr::from_bytes_with_nul(b"/chosen\0").unwrap();
     let bootargs_name = CStr::from_bytes_with_nul(b"bootargs\0").unwrap();
 
@@ -129,6 +125,63 @@
     }
 }
 
+/// Enables console output by adding kernel.printk.devkmsg and kernel.console to bootargs.
+/// This uses hardcoded console name 'hvc0' and it should be match with microdroid's bootconfig.debuggable.
+fn enable_console_output(fdt: &mut libfdt::Fdt) -> Result<(), DebugPolicyError> {
+    let chosen_path = CStr::from_bytes_with_nul(b"/chosen\0").unwrap();
+    let bootargs_name = CStr::from_bytes_with_nul(b"bootargs\0").unwrap();
+
+    let chosen = match fdt
+        .node(chosen_path)
+        .map_err(|e| DebugPolicyError::Fdt("Failed to find /chosen", e))?
+    {
+        Some(node) => node,
+        None => return Ok(()),
+    };
+
+    let bootargs = match chosen
+        .getprop_str(bootargs_name)
+        .map_err(|e| DebugPolicyError::Fdt("Failed to find bootargs prop", e))?
+    {
+        Some(value) if !value.to_bytes().is_empty() => value,
+        _ => return Ok(()),
+    };
+
+    let mut new_bootargs = Vec::from(bootargs.to_bytes());
+    new_bootargs.extend_from_slice(b" printk.devkmsg=on console=hvc0\0");
+
+    // We'll set larger prop, and need to prepare some room first.
+    fdt.unpack().map_err(|e| DebugPolicyError::OverlaidFdt("Failed to unpack", e))?;
+
+    // We've checked existence of /chosen node at the beginning.
+    let mut chosen_mut = fdt.node_mut(chosen_path).unwrap().unwrap();
+    chosen_mut.setprop(bootargs_name, new_bootargs.as_slice()).map_err(|e| {
+        DebugPolicyError::OverlaidFdt("Failed to enabled console output. FDT might be corrupted", e)
+    })?;
+
+    fdt.pack().map_err(|e| DebugPolicyError::OverlaidFdt("Failed to pack", e))?;
+    Ok(())
+}
+
+/// Returns true only if fdt has log prop in the /avf/guest/common node with value <1>
+fn is_console_output_enabled(fdt: &libfdt::Fdt) -> Result<bool, DebugPolicyError> {
+    let common = match fdt
+        .node(CStr::from_bytes_with_nul(b"/avf/guest/common\0").unwrap())
+        .map_err(|e| DebugPolicyError::DebugPolicyFdt("Failed to find /avf/guest/common node", e))?
+    {
+        Some(node) => node,
+        None => return Ok(false),
+    };
+
+    match common
+        .getprop_u32(CStr::from_bytes_with_nul(b"log\0").unwrap())
+        .map_err(|e| DebugPolicyError::DebugPolicyFdt("Failed to find log prop", e))?
+    {
+        Some(1) => Ok(true),
+        _ => Ok(false),
+    }
+}
+
 /// Handles debug policies.
 ///
 /// # Safety
@@ -146,7 +199,14 @@
     // Handles ramdump in the debug policy
     if is_ramdump_enabled(fdt)? {
         info!("ramdump is enabled by debug policy");
-        return Ok(());
+    } else {
+        disable_ramdump(fdt)?;
     }
-    disable_ramdump(fdt)
+
+    // Handles console output in the debug policy
+    if is_console_output_enabled(fdt)? {
+        enable_console_output(fdt)?;
+        info!("console output is enabled by debug policy");
+    }
+    Ok(())
 }
diff --git a/pvmfw/src/dice.rs b/pvmfw/src/dice.rs
index 49218b0..3ceb8ef 100644
--- a/pvmfw/src/dice.rs
+++ b/pvmfw/src/dice.rs
@@ -14,14 +14,15 @@
 
 //! Support for DICE derivation and BCC generation.
 
+use crate::helpers::flushed_zeroize;
+use core::ffi::c_void;
 use core::ffi::CStr;
 use core::mem::size_of;
-use dice::bcc::format_config_descriptor;
-use dice::bcc::Handover;
-use dice::hash;
-use dice::ConfigType;
-use dice::DiceMode;
-use dice::InputValues;
+use core::slice;
+
+use diced_open_dice::{
+    bcc_format_config_descriptor, hash, Config, DiceMode, Hash, InputValues, HIDDEN_SIZE,
+};
 use pvmfw_avb::{DebugLevel, Digest, VerifiedBootData};
 
 fn to_dice_mode(debug_level: DebugLevel) -> DiceMode {
@@ -31,7 +32,7 @@
     }
 }
 
-fn to_dice_hash(verified_boot_data: &VerifiedBootData) -> dice::Result<dice::Hash> {
+fn to_dice_hash(verified_boot_data: &VerifiedBootData) -> diced_open_dice::Result<Hash> {
     let mut digests = [0u8; size_of::<Digest>() * 2];
     digests[..size_of::<Digest>()].copy_from_slice(&verified_boot_data.kernel_digest);
     if let Some(initrd_digest) = verified_boot_data.initrd_digest {
@@ -40,36 +41,54 @@
     hash(&digests)
 }
 
-/// Derive the VM-specific secrets and certificate through DICE.
-pub fn derive_next_bcc(
-    bcc: &Handover,
-    next_bcc: &mut [u8],
-    verified_boot_data: &VerifiedBootData,
-    authority: &[u8],
-) -> dice::Result<usize> {
-    let code_hash = to_dice_hash(verified_boot_data)?;
-    let auth_hash = hash(authority)?;
-    let mode = to_dice_mode(verified_boot_data.debug_level);
-    let component_name = CStr::from_bytes_with_nul(b"vm_entry\0").unwrap();
-    let mut config_descriptor_buffer = [0; 128];
-    let config_descriptor_size = format_config_descriptor(
-        &mut config_descriptor_buffer,
-        Some(component_name),
-        None,  // component_version
-        false, // resettable
-    )?;
-    let config = &config_descriptor_buffer[..config_descriptor_size];
-    let config = ConfigType::Descriptor(config);
+pub struct PartialInputs {
+    pub code_hash: Hash,
+    pub auth_hash: Hash,
+    pub mode: DiceMode,
+}
 
-    let input_values = InputValues::new(
-        &code_hash,
-        None, // code_descriptor
-        &config,
-        Some(&auth_hash),
-        None, // auth_descriptor
-        mode,
-        None, // TODO(b/249723852): Get salt from instance.img (virtio-blk) and/or TRNG.
-    );
+impl PartialInputs {
+    pub fn new(data: &VerifiedBootData) -> diced_open_dice::Result<Self> {
+        let code_hash = to_dice_hash(data)?;
+        let auth_hash = hash(data.public_key)?;
+        let mode = to_dice_mode(data.debug_level);
 
-    bcc.main_flow(&input_values, next_bcc)
+        Ok(Self { code_hash, auth_hash, mode })
+    }
+
+    pub fn into_input_values(
+        self,
+        salt: &[u8; HIDDEN_SIZE],
+    ) -> diced_open_dice::Result<InputValues> {
+        let component_name = CStr::from_bytes_with_nul(b"vm_entry\0").unwrap();
+        let mut config_descriptor_buffer = [0; 128];
+        let config_descriptor_size = bcc_format_config_descriptor(
+            Some(component_name),
+            None,  // component_version
+            false, // resettable
+            &mut config_descriptor_buffer,
+        )?;
+        let config = &config_descriptor_buffer[..config_descriptor_size];
+
+        Ok(InputValues::new(
+            self.code_hash,
+            Config::Descriptor(config),
+            self.auth_hash,
+            self.mode,
+            *salt,
+        ))
+    }
+}
+
+/// Flushes data caches over the provided address range.
+///
+/// # Safety
+///
+/// The provided address and size must be to a valid address range (typically on the stack, .bss,
+/// .data, or provided BCC).
+#[no_mangle]
+unsafe extern "C" fn DiceClearMemory(_ctx: *mut c_void, size: usize, addr: *mut c_void) {
+    // SAFETY - We must trust that the slice will be valid arrays/variables on the C code stack.
+    let region = unsafe { slice::from_raw_parts_mut(addr as *mut u8, size) };
+    flushed_zeroize(region)
 }
diff --git a/pvmfw/src/entry.rs b/pvmfw/src/entry.rs
index 530449c..106a4ef 100644
--- a/pvmfw/src/entry.rs
+++ b/pvmfw/src/entry.rs
@@ -22,10 +22,10 @@
 use crate::memory::MemoryTracker;
 use crate::mmio_guard;
 use crate::mmu;
+use crate::rand;
 use core::arch::asm;
 use core::num::NonZeroUsize;
 use core::slice;
-use dice::bcc::Handover;
 use log::debug;
 use log::error;
 use log::info;
@@ -242,11 +242,7 @@
         RebootReason::InvalidConfig
     })?;
 
-    let bcc_slice = appended.get_bcc_mut();
-    let bcc = Handover::new(bcc_slice).map_err(|e| {
-        error!("Invalid BCC Handover: {e:?}");
-        RebootReason::InvalidBcc
-    })?;
+    let (bcc_slice, debug_policy) = appended.get_entries();
 
     debug!("Activating dynamic page table...");
     // SAFETY - page_table duplicates the static mappings for everything that the Rust code is
@@ -257,15 +253,20 @@
     let mut memory = MemoryTracker::new(page_table);
     let slices = MemorySlices::new(fdt, payload, payload_size, &mut memory)?;
 
+    rand::init().map_err(|e| {
+        error!("Failed to initialize rand: {e}");
+        RebootReason::InternalError
+    })?;
+
     // This wrapper allows main() to be blissfully ignorant of platform details.
-    crate::main(slices.fdt, slices.kernel, slices.ramdisk, &bcc, &mut memory)?;
+    crate::main(slices.fdt, slices.kernel, slices.ramdisk, bcc_slice, &mut memory)?;
 
     helpers::flushed_zeroize(bcc_slice);
     helpers::flush(slices.fdt.as_slice());
 
     // SAFETY - As we `?` the result, there is no risk of using a bad `slices.fdt`.
     unsafe {
-        handle_debug_policy(slices.fdt, appended.get_debug_policy()).map_err(|e| {
+        handle_debug_policy(slices.fdt, debug_policy).map_err(|e| {
             error!("Unexpected error when handling debug policy: {e:?}");
             RebootReason::from(e)
         })?;
@@ -396,18 +397,10 @@
         }
     }
 
-    #[allow(dead_code)] // TODO(b/232900974)
-    fn get_debug_policy(&mut self) -> Option<&mut [u8]> {
+    fn get_entries(&mut self) -> (&mut [u8], Option<&mut [u8]>) {
         match self {
-            Self::Config(ref mut cfg) => cfg.get_debug_policy(),
-            Self::LegacyBcc(_) => None,
-        }
-    }
-
-    fn get_bcc_mut(&mut self) -> &mut [u8] {
-        match self {
-            Self::LegacyBcc(ref mut bcc) => bcc,
-            Self::Config(ref mut cfg) => cfg.get_bcc_mut(),
+            Self::Config(ref mut cfg) => cfg.get_entries(),
+            Self::LegacyBcc(ref mut bcc) => (bcc, None),
         }
     }
 }
diff --git a/pvmfw/src/fdt.rs b/pvmfw/src/fdt.rs
index b735b9c..793eaac 100644
--- a/pvmfw/src/fdt.rs
+++ b/pvmfw/src/fdt.rs
@@ -16,6 +16,8 @@
 
 use core::ffi::CStr;
 use core::ops::Range;
+use libfdt::Fdt;
+use libfdt::FdtError;
 
 /// Extract from /config the address range containing the pre-loaded kernel.
 pub fn kernel_range(fdt: &libfdt::Fdt) -> libfdt::Result<Option<Range<usize>>> {
@@ -49,10 +51,35 @@
     Ok(None)
 }
 
-/// Add a "google,open-dice"-compatible reserved-memory node to the tree.
-pub fn add_dice_node(fdt: &mut libfdt::Fdt, addr: usize, size: usize) -> libfdt::Result<()> {
+/// Modifies the input DT according to the fields of the configuration.
+pub fn modify_for_next_stage(
+    fdt: &mut Fdt,
+    bcc: &[u8],
+    new_instance: bool,
+    strict_boot: bool,
+) -> libfdt::Result<()> {
     fdt.unpack()?;
 
+    add_dice_node(fdt, bcc.as_ptr() as usize, bcc.len())?;
+
+    set_or_clear_chosen_flag(
+        fdt,
+        CStr::from_bytes_with_nul(b"avf,strict-boot\0").unwrap(),
+        strict_boot,
+    )?;
+    set_or_clear_chosen_flag(
+        fdt,
+        CStr::from_bytes_with_nul(b"avf,new-instance\0").unwrap(),
+        new_instance,
+    )?;
+
+    fdt.pack()?;
+
+    Ok(())
+}
+
+/// Add a "google,open-dice"-compatible reserved-memory node to the tree.
+fn add_dice_node(fdt: &mut Fdt, addr: usize, size: usize) -> libfdt::Result<()> {
     let reserved_memory = CStr::from_bytes_with_nul(b"/reserved-memory\0").unwrap();
     // We reject DTs with missing reserved-memory node as validation should have checked that the
     // "swiotlb" subnode (compatible = "restricted-dma-pool") was present.
@@ -67,10 +94,25 @@
     let no_map = CStr::from_bytes_with_nul(b"no-map\0").unwrap();
     dice.appendprop(no_map, &[])?;
 
+    let addr = addr.try_into().unwrap();
+    let size = size.try_into().unwrap();
     let reg = CStr::from_bytes_with_nul(b"reg\0").unwrap();
-    dice.appendprop_addrrange(reg, addr as u64, size as u64)?;
+    dice.appendprop_addrrange(reg, addr, size)?;
 
-    fdt.pack()?;
+    Ok(())
+}
+
+fn set_or_clear_chosen_flag(fdt: &mut Fdt, flag: &CStr, value: bool) -> libfdt::Result<()> {
+    // TODO(b/249054080): Refactor to not panic if the DT doesn't contain a /chosen node.
+    let mut chosen = fdt.chosen_mut()?.unwrap();
+    if value {
+        chosen.setprop_empty(flag)?;
+    } else {
+        match chosen.delprop(flag) {
+            Ok(()) | Err(FdtError::NotFound) => (),
+            Err(e) => return Err(e),
+        }
+    }
 
     Ok(())
 }
diff --git a/pvmfw/src/gpt.rs b/pvmfw/src/gpt.rs
new file mode 100644
index 0000000..6af3047
--- /dev/null
+++ b/pvmfw/src/gpt.rs
@@ -0,0 +1,253 @@
+// Copyright 2023, 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.
+
+//! Support for parsing GUID partition tables.
+
+use crate::helpers::ceiling_div;
+use crate::virtio::pci::VirtIOBlk;
+use core::cmp::min;
+use core::fmt;
+use core::mem::size_of;
+use core::ops::RangeInclusive;
+use core::slice;
+use static_assertions::const_assert;
+use static_assertions::const_assert_eq;
+use uuid::Uuid;
+use virtio_drivers::device::blk::SECTOR_SIZE;
+
+pub enum Error {
+    /// VirtIO error during read operation.
+    FailedRead(virtio_drivers::Error),
+    /// VirtIO error during write operation.
+    FailedWrite(virtio_drivers::Error),
+    /// Invalid GPT header.
+    InvalidHeader,
+    /// Invalid partition block index.
+    BlockOutsidePartition(usize),
+}
+
+impl fmt::Display for Error {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Self::FailedRead(e) => write!(f, "Failed to read from disk: {e}"),
+            Self::FailedWrite(e) => write!(f, "Failed to write to disk: {e}"),
+            Self::InvalidHeader => write!(f, "Found invalid GPT header"),
+            Self::BlockOutsidePartition(i) => write!(f, "Accessed invalid block index {i}"),
+        }
+    }
+}
+
+pub type Result<T> = core::result::Result<T, Error>;
+
+pub struct Partition {
+    partitions: Partitions,
+    indices: RangeInclusive<usize>,
+}
+
+impl Partition {
+    pub fn get_by_name(device: VirtIOBlk, name: &str) -> Result<Option<Self>> {
+        Partitions::new(device)?.get_partition_by_name(name)
+    }
+
+    fn new(partitions: Partitions, entry: &Entry) -> Self {
+        let first = entry.first_lba().try_into().unwrap();
+        let last = entry.last_lba().try_into().unwrap();
+
+        Self { partitions, indices: first..=last }
+    }
+
+    pub fn indices(&self) -> RangeInclusive<usize> {
+        self.indices.clone()
+    }
+
+    pub fn read_block(&mut self, index: usize, blk: &mut [u8]) -> Result<()> {
+        let index = self.block_index(index).ok_or(Error::BlockOutsidePartition(index))?;
+        self.partitions.read_block(index, blk)
+    }
+
+    pub fn write_block(&mut self, index: usize, blk: &[u8]) -> Result<()> {
+        let index = self.block_index(index).ok_or(Error::BlockOutsidePartition(index))?;
+        self.partitions.write_block(index, blk)
+    }
+
+    fn block_index(&self, index: usize) -> Option<usize> {
+        if self.indices.contains(&index) {
+            Some(index)
+        } else {
+            None
+        }
+    }
+}
+
+pub struct Partitions {
+    device: VirtIOBlk,
+    entries_count: usize,
+}
+
+impl Partitions {
+    pub const LBA_SIZE: usize = SECTOR_SIZE;
+
+    fn new(mut device: VirtIOBlk) -> Result<Self> {
+        let mut blk = [0; Self::LBA_SIZE];
+        device.read_block(Header::LBA, &mut blk).map_err(Error::FailedRead)?;
+        let (header_bytes, _) = blk.split_at(size_of::<Header>());
+        let header = Header::from_bytes(header_bytes).ok_or(Error::InvalidHeader)?;
+        let entries_count = usize::try_from(header.entries_count()).unwrap();
+
+        Ok(Self { device, entries_count })
+    }
+
+    fn get_partition_by_name(mut self, name: &str) -> Result<Option<Partition>> {
+        const_assert_eq!(Partitions::LBA_SIZE.rem_euclid(size_of::<Entry>()), 0);
+        let entries_per_blk = Partitions::LBA_SIZE.checked_div(size_of::<Entry>()).unwrap();
+
+        // Create a UTF-16 reference against which we'll compare partition names. Note that unlike
+        // the C99 wcslen(), this comparison will cover bytes past the first L'\0' character.
+        let mut needle = [0; Entry::NAME_SIZE / size_of::<u16>()];
+        for (dest, src) in needle.iter_mut().zip(name.encode_utf16()) {
+            *dest = src;
+        }
+
+        let mut blk = [0; Self::LBA_SIZE];
+        let mut rem = self.entries_count;
+        let num_blocks = ceiling_div(self.entries_count, entries_per_blk).unwrap();
+        for i in Header::ENTRIES_LBA..Header::ENTRIES_LBA.checked_add(num_blocks).unwrap() {
+            self.read_block(i, &mut blk)?;
+            let entries = blk.as_ptr().cast::<Entry>();
+            // SAFETY - blk is assumed to be properly aligned for Entry and its size is assert-ed
+            // above. All potential values of the slice will produce valid Entry values.
+            let entries = unsafe { slice::from_raw_parts(entries, min(rem, entries_per_blk)) };
+            for entry in entries {
+                let entry_name = entry.name;
+                if entry_name == needle {
+                    return Ok(Some(Partition::new(self, entry)));
+                }
+                rem -= 1;
+            }
+        }
+        Ok(None)
+    }
+
+    fn read_block(&mut self, index: usize, blk: &mut [u8]) -> Result<()> {
+        self.device.read_block(index, blk).map_err(Error::FailedRead)
+    }
+
+    fn write_block(&mut self, index: usize, blk: &[u8]) -> Result<()> {
+        self.device.write_block(index, blk).map_err(Error::FailedWrite)
+    }
+}
+
+type Lba = u64;
+
+/// Structure as defined in release 2.10 of the UEFI Specification (5.3.2 GPT Header).
+#[repr(C, packed)]
+struct Header {
+    signature: u64,
+    revision: u32,
+    header_size: u32,
+    header_crc32: u32,
+    reserved0: u32,
+    current_lba: Lba,
+    backup_lba: Lba,
+    first_lba: Lba,
+    last_lba: Lba,
+    disk_guid: Uuid,
+    entries_lba: Lba,
+    entries_count: u32,
+    entry_size: u32,
+    entries_crc32: u32,
+}
+const_assert!(size_of::<Header>() < Partitions::LBA_SIZE);
+
+impl Header {
+    const SIGNATURE: u64 = u64::from_le_bytes(*b"EFI PART");
+    const REVISION_1_0: u32 = 1 << 16;
+    const LBA: usize = 1;
+    const ENTRIES_LBA: usize = 2;
+
+    fn from_bytes(bytes: &[u8]) -> Option<&Self> {
+        let bytes = bytes.get(..size_of::<Self>())?;
+        // SAFETY - We assume that bytes is properly aligned for Header and have verified above
+        // that it holds enough bytes. All potential values of the slice will produce a valid
+        // Header.
+        let header = unsafe { &*bytes.as_ptr().cast::<Self>() };
+
+        if header.is_valid() {
+            Some(header)
+        } else {
+            None
+        }
+    }
+
+    fn is_valid(&self) -> bool {
+        self.signature() == Self::SIGNATURE
+            && self.header_size() == size_of::<Self>().try_into().unwrap()
+            && self.revision() == Self::REVISION_1_0
+            && self.entry_size() == size_of::<Entry>().try_into().unwrap()
+            && self.current_lba() == Self::LBA.try_into().unwrap()
+            && self.entries_lba() == Self::ENTRIES_LBA.try_into().unwrap()
+    }
+
+    fn signature(&self) -> u64 {
+        u64::from_le(self.signature)
+    }
+
+    fn entries_count(&self) -> u32 {
+        u32::from_le(self.entries_count)
+    }
+
+    fn header_size(&self) -> u32 {
+        u32::from_le(self.header_size)
+    }
+
+    fn revision(&self) -> u32 {
+        u32::from_le(self.revision)
+    }
+
+    fn entry_size(&self) -> u32 {
+        u32::from_le(self.entry_size)
+    }
+
+    fn entries_lba(&self) -> Lba {
+        Lba::from_le(self.entries_lba)
+    }
+
+    fn current_lba(&self) -> Lba {
+        Lba::from_le(self.current_lba)
+    }
+}
+
+/// Structure as defined in release 2.10 of the UEFI Specification (5.3.3 GPT Partition Entry
+/// Array).
+#[repr(C, packed)]
+struct Entry {
+    type_guid: Uuid,
+    guid: Uuid,
+    first_lba: Lba,
+    last_lba: Lba,
+    flags: u64,
+    name: [u16; Entry::NAME_SIZE / size_of::<u16>()], // UTF-16
+}
+
+impl Entry {
+    const NAME_SIZE: usize = 72;
+
+    fn first_lba(&self) -> Lba {
+        Lba::from_le(self.first_lba)
+    }
+
+    fn last_lba(&self) -> Lba {
+        Lba::from_le(self.last_lba)
+    }
+}
diff --git a/pvmfw/src/helpers.rs b/pvmfw/src/helpers.rs
index 40266f7..e6e3406 100644
--- a/pvmfw/src/helpers.rs
+++ b/pvmfw/src/helpers.rs
@@ -47,6 +47,17 @@
     }
 }
 
+/// Performs an integer division rounding up.
+///
+/// Note: Returns None if den isn't a power of two.
+pub const fn ceiling_div(num: usize, den: usize) -> Option<usize> {
+    let Some(r) = align_up(num, den) else {
+        return None;
+    };
+
+    r.checked_div(den)
+}
+
 /// Aligns the given address to the given alignment, if it is a power of two.
 ///
 /// Returns `None` if the alignment isn't a power of two.
diff --git a/pvmfw/src/hvc.rs b/pvmfw/src/hvc.rs
index dc99303..08edd86 100644
--- a/pvmfw/src/hvc.rs
+++ b/pvmfw/src/hvc.rs
@@ -14,9 +14,19 @@
 
 //! Wrappers around calls to the hypervisor.
 
+pub mod trng;
+
 use crate::smccc::{self, checked_hvc64, checked_hvc64_expect_zero};
 use log::info;
 
+const ARM_SMCCC_TRNG_VERSION: u32 = 0x8400_0050;
+#[allow(dead_code)]
+const ARM_SMCCC_TRNG_FEATURES: u32 = 0x8400_0051;
+#[allow(dead_code)]
+const ARM_SMCCC_TRNG_GET_UUID: u32 = 0x8400_0052;
+#[allow(dead_code)]
+const ARM_SMCCC_TRNG_RND32: u32 = 0x8400_0053;
+const ARM_SMCCC_TRNG_RND64: u32 = 0xc400_0053;
 const ARM_SMCCC_KVM_FUNC_HYP_MEMINFO: u32 = 0xc6000002;
 const ARM_SMCCC_KVM_FUNC_MEM_SHARE: u32 = 0xc6000003;
 const ARM_SMCCC_KVM_FUNC_MEM_UNSHARE: u32 = 0xc6000004;
@@ -28,14 +38,14 @@
 /// Queries the memory protection parameters for a protected virtual machine.
 ///
 /// Returns the memory protection granule size in bytes.
-pub fn hyp_meminfo() -> smccc::Result<u64> {
+pub fn kvm_hyp_meminfo() -> smccc::Result<u64> {
     let args = [0u64; 17];
     checked_hvc64(ARM_SMCCC_KVM_FUNC_HYP_MEMINFO, args)
 }
 
 /// Shares a region of memory with the KVM host, granting it read, write and execute permissions.
 /// The size of the region is equal to the memory protection granule returned by [`hyp_meminfo`].
-pub fn mem_share(base_ipa: u64) -> smccc::Result<()> {
+pub fn kvm_mem_share(base_ipa: u64) -> smccc::Result<()> {
     let mut args = [0u64; 17];
     args[0] = base_ipa;
 
@@ -45,26 +55,26 @@
 /// Revokes access permission from the KVM host to a memory region previously shared with
 /// [`mem_share`]. The size of the region is equal to the memory protection granule returned by
 /// [`hyp_meminfo`].
-pub fn mem_unshare(base_ipa: u64) -> smccc::Result<()> {
+pub fn kvm_mem_unshare(base_ipa: u64) -> smccc::Result<()> {
     let mut args = [0u64; 17];
     args[0] = base_ipa;
 
     checked_hvc64_expect_zero(ARM_SMCCC_KVM_FUNC_MEM_UNSHARE, args)
 }
 
-pub fn mmio_guard_info() -> smccc::Result<u64> {
+pub fn kvm_mmio_guard_info() -> smccc::Result<u64> {
     let args = [0u64; 17];
 
     checked_hvc64(VENDOR_HYP_KVM_MMIO_GUARD_INFO_FUNC_ID, args)
 }
 
-pub fn mmio_guard_enroll() -> smccc::Result<()> {
+pub fn kvm_mmio_guard_enroll() -> smccc::Result<()> {
     let args = [0u64; 17];
 
     checked_hvc64_expect_zero(VENDOR_HYP_KVM_MMIO_GUARD_ENROLL_FUNC_ID, args)
 }
 
-pub fn mmio_guard_map(ipa: u64) -> smccc::Result<()> {
+pub fn kvm_mmio_guard_map(ipa: u64) -> smccc::Result<()> {
     let mut args = [0u64; 17];
     args[0] = ipa;
 
@@ -84,7 +94,7 @@
     }
 }
 
-pub fn mmio_guard_unmap(ipa: u64) -> smccc::Result<()> {
+pub fn kvm_mmio_guard_unmap(ipa: u64) -> smccc::Result<()> {
     let mut args = [0u64; 17];
     args[0] = ipa;
 
@@ -94,3 +104,22 @@
         x => x,
     }
 }
+
+/// Returns the (major, minor) version tuple, as defined by the SMCCC TRNG.
+pub fn trng_version() -> trng::Result<(u16, u16)> {
+    let args = [0u64; 17];
+
+    let version = trng::hvc64(ARM_SMCCC_TRNG_VERSION, args)?[0];
+    Ok(((version >> 16) as u16, version as u16))
+}
+
+pub type TrngRng64Entropy = (u64, u64, u64);
+
+pub fn trng_rnd64(nbits: u64) -> trng::Result<TrngRng64Entropy> {
+    let mut args = [0u64; 17];
+    args[0] = nbits;
+
+    let regs = trng::hvc64_expect_zero(ARM_SMCCC_TRNG_RND64, args)?;
+
+    Ok((regs[1], regs[2], regs[3]))
+}
diff --git a/pvmfw/src/hvc/trng.rs b/pvmfw/src/hvc/trng.rs
new file mode 100644
index 0000000..d347693
--- /dev/null
+++ b/pvmfw/src/hvc/trng.rs
@@ -0,0 +1,65 @@
+// Copyright 2023, 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.
+
+use crate::smccc;
+use core::fmt;
+use core::result;
+
+/// Standard SMCCC TRNG error values as described in DEN 0098 1.0 REL0.
+#[derive(Debug, Clone)]
+pub enum Error {
+    /// The call is not supported by the implementation.
+    NotSupported,
+    /// One of the call parameters has a non-supported value.
+    InvalidParameter,
+    /// Call returned without the requested entropy bits.
+    NoEntropy,
+    /// Negative values indicate error.
+    Unknown(i64),
+    /// The call returned a positive value when 0 was expected.
+    Unexpected(u64),
+}
+
+impl fmt::Display for Error {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Self::NotSupported => write!(f, "SMCCC TRNG call not supported"),
+            Self::InvalidParameter => write!(f, "SMCCC TRNG call received non-supported value"),
+            Self::NoEntropy => write!(f, "SMCCC TRNG call returned no entropy"),
+            Self::Unexpected(v) => write!(f, "Unexpected SMCCC TRNG return value {} ({0:#x})", v),
+            Self::Unknown(e) => write!(f, "Unknown SMCCC TRNG return value {} ({0:#x})", e),
+        }
+    }
+}
+
+pub type Result<T> = result::Result<T, Error>;
+
+pub fn hvc64(function: u32, args: [u64; 17]) -> Result<[u64; 18]> {
+    let res = smccc::hvc64(function, args);
+    match res[0] as i64 {
+        ret if ret >= 0 => Ok(res),
+        -1 => Err(Error::NotSupported),
+        -2 => Err(Error::InvalidParameter),
+        -3 => Err(Error::NoEntropy),
+        ret => Err(Error::Unknown(ret)),
+    }
+}
+
+pub fn hvc64_expect_zero(function: u32, args: [u64; 17]) -> Result<[u64; 18]> {
+    let res = hvc64(function, args)?;
+    match res[0] {
+        0 => Ok(res),
+        v => Err(Error::Unexpected(v)),
+    }
+}
diff --git a/pvmfw/src/hypervisor.rs b/pvmfw/src/hypervisor.rs
new file mode 100644
index 0000000..e06d809
--- /dev/null
+++ b/pvmfw/src/hypervisor.rs
@@ -0,0 +1,46 @@
+// Copyright 2023, 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.
+
+//! Wrappers around hypervisor back-ends.
+
+use crate::hvc;
+use crate::smccc;
+
+pub fn hyp_meminfo() -> smccc::Result<u64> {
+    hvc::kvm_hyp_meminfo()
+}
+
+pub fn mem_share(base_ipa: u64) -> smccc::Result<()> {
+    hvc::kvm_mem_share(base_ipa)
+}
+
+pub fn mem_unshare(base_ipa: u64) -> smccc::Result<()> {
+    hvc::kvm_mem_unshare(base_ipa)
+}
+
+pub fn mmio_guard_info() -> smccc::Result<u64> {
+    hvc::kvm_mmio_guard_info()
+}
+
+pub fn mmio_guard_enroll() -> smccc::Result<()> {
+    hvc::kvm_mmio_guard_enroll()
+}
+
+pub fn mmio_guard_map(ipa: u64) -> smccc::Result<()> {
+    hvc::kvm_mmio_guard_map(ipa)
+}
+
+pub fn mmio_guard_unmap(ipa: u64) -> smccc::Result<()> {
+    hvc::kvm_mmio_guard_unmap(ipa)
+}
diff --git a/pvmfw/src/instance.rs b/pvmfw/src/instance.rs
new file mode 100644
index 0000000..fbf2040
--- /dev/null
+++ b/pvmfw/src/instance.rs
@@ -0,0 +1,338 @@
+// Copyright 2023, 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.
+
+//! Support for reading and writing to the instance.img.
+
+use crate::crypto;
+use crate::crypto::hkdf_sh512;
+use crate::crypto::AeadCtx;
+use crate::dice::PartialInputs;
+use crate::gpt;
+use crate::gpt::Partition;
+use crate::gpt::Partitions;
+use crate::helpers::ceiling_div;
+use crate::rand;
+use crate::virtio::pci::VirtIOBlkIterator;
+use core::fmt;
+use core::mem::size_of;
+use core::slice;
+use diced_open_dice::DiceMode;
+use diced_open_dice::Hash;
+use diced_open_dice::Hidden;
+use log::trace;
+use uuid::Uuid;
+use virtio_drivers::transport::pci::bus::PciRoot;
+
+pub enum Error {
+    /// Unexpected I/O error while accessing the underlying disk.
+    FailedIo(gpt::Error),
+    /// Failed to decrypt the entry.
+    FailedOpen(crypto::ErrorIterator),
+    /// Failed to generate a random salt to be stored.
+    FailedSaltGeneration(rand::Error),
+    /// Failed to encrypt the entry.
+    FailedSeal(crypto::ErrorIterator),
+    /// Impossible to create a new instance.img entry.
+    InstanceImageFull,
+    /// Badly formatted instance.img header block.
+    InvalidInstanceImageHeader,
+    /// No instance.img ("vm-instance") partition found.
+    MissingInstanceImage,
+    /// The instance.img doesn't contain a header.
+    MissingInstanceImageHeader,
+    /// Authority hash found in the pvmfw instance.img entry doesn't match the trusted public key.
+    RecordedAuthHashMismatch,
+    /// Code hash found in the pvmfw instance.img entry doesn't match the inputs.
+    RecordedCodeHashMismatch,
+    /// DICE mode found in the pvmfw instance.img entry doesn't match the current one.
+    RecordedDiceModeMismatch,
+    /// Size of the instance.img entry being read or written is not supported.
+    UnsupportedEntrySize(usize),
+}
+
+impl fmt::Display for Error {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Self::FailedIo(e) => write!(f, "Failed I/O to disk: {e}"),
+            Self::FailedOpen(e_iter) => {
+                writeln!(f, "Failed to open the instance.img partition:")?;
+                for e in *e_iter {
+                    writeln!(f, "\t{e}")?;
+                }
+                Ok(())
+            }
+            Self::FailedSaltGeneration(e) => write!(f, "Failed to generate salt: {e}"),
+            Self::FailedSeal(e_iter) => {
+                writeln!(f, "Failed to seal the instance.img partition:")?;
+                for e in *e_iter {
+                    writeln!(f, "\t{e}")?;
+                }
+                Ok(())
+            }
+            Self::InstanceImageFull => write!(f, "Failed to obtain a free instance.img partition"),
+            Self::InvalidInstanceImageHeader => write!(f, "instance.img header is invalid"),
+            Self::MissingInstanceImage => write!(f, "Failed to find the instance.img partition"),
+            Self::MissingInstanceImageHeader => write!(f, "instance.img header is missing"),
+            Self::RecordedAuthHashMismatch => write!(f, "Recorded authority hash doesn't match"),
+            Self::RecordedCodeHashMismatch => write!(f, "Recorded code hash doesn't match"),
+            Self::RecordedDiceModeMismatch => write!(f, "Recorded DICE mode doesn't match"),
+            Self::UnsupportedEntrySize(sz) => write!(f, "Invalid entry size: {sz}"),
+        }
+    }
+}
+
+pub type Result<T> = core::result::Result<T, Error>;
+
+pub fn get_or_generate_instance_salt(
+    pci_root: &mut PciRoot,
+    dice_inputs: &PartialInputs,
+    secret: &[u8],
+) -> Result<(bool, Hidden)> {
+    let mut instance_img = find_instance_img(pci_root)?;
+
+    let entry = locate_entry(&mut instance_img)?;
+    trace!("Found pvmfw instance.img entry: {entry:?}");
+
+    let key = hkdf_sh512::<32>(secret, /*salt=*/ &[], b"vm-instance");
+    let mut blk = [0; BLK_SIZE];
+    match entry {
+        PvmfwEntry::Existing { header_index, payload_size } => {
+            if payload_size > blk.len() {
+                // We currently only support single-blk entries.
+                return Err(Error::UnsupportedEntrySize(payload_size));
+            }
+            let payload_index = header_index + 1;
+            instance_img.read_block(payload_index, &mut blk).map_err(Error::FailedIo)?;
+
+            let payload = &blk[..payload_size];
+            let mut entry = [0; size_of::<EntryBody>()];
+            let key = key.map_err(Error::FailedOpen)?;
+            let aead = AeadCtx::new_aes_256_gcm_randnonce(&key).map_err(Error::FailedOpen)?;
+            let decrypted = aead.open(&mut entry, payload).map_err(Error::FailedOpen)?;
+
+            let body: &EntryBody = decrypted.as_ref();
+            if body.code_hash != dice_inputs.code_hash {
+                Err(Error::RecordedCodeHashMismatch)
+            } else if body.auth_hash != dice_inputs.auth_hash {
+                Err(Error::RecordedAuthHashMismatch)
+            } else if body.mode() != dice_inputs.mode {
+                Err(Error::RecordedDiceModeMismatch)
+            } else {
+                Ok((false, body.salt))
+            }
+        }
+        PvmfwEntry::New { header_index } => {
+            let salt = rand::random_array().map_err(Error::FailedSaltGeneration)?;
+            let entry_body = EntryBody::new(dice_inputs, &salt);
+            let body = entry_body.as_ref();
+
+            let key = key.map_err(Error::FailedSeal)?;
+            let aead = AeadCtx::new_aes_256_gcm_randnonce(&key).map_err(Error::FailedSeal)?;
+            // We currently only support single-blk entries.
+            assert!(body.len() + aead.aead().unwrap().max_overhead() < blk.len());
+            let encrypted = aead.seal(&mut blk, body).map_err(Error::FailedSeal)?;
+            let payload_size = encrypted.len();
+            let payload_index = header_index + 1;
+            instance_img.write_block(payload_index, &blk).map_err(Error::FailedIo)?;
+
+            let header = EntryHeader::new(PvmfwEntry::UUID, payload_size);
+            let (blk_header, blk_rest) = blk.split_at_mut(size_of::<EntryHeader>());
+            blk_header.copy_from_slice(header.as_ref());
+            blk_rest.fill(0);
+            instance_img.write_block(header_index, &blk).map_err(Error::FailedIo)?;
+
+            Ok((true, salt))
+        }
+    }
+}
+
+#[repr(C, packed)]
+struct Header {
+    magic: [u8; Header::MAGIC.len()],
+    version: u16,
+}
+
+impl Header {
+    const MAGIC: &[u8] = b"Android-VM-instance";
+    const VERSION_1: u16 = 1;
+
+    pub fn is_valid(&self) -> bool {
+        self.magic == Self::MAGIC && self.version() == Self::VERSION_1
+    }
+
+    fn version(&self) -> u16 {
+        u16::from_le(self.version)
+    }
+
+    fn from_bytes(bytes: &[u8]) -> Option<&Self> {
+        let header: &Self = bytes.as_ref();
+
+        if header.is_valid() {
+            Some(header)
+        } else {
+            None
+        }
+    }
+}
+
+impl AsRef<Header> for [u8] {
+    fn as_ref(&self) -> &Header {
+        // SAFETY - Assume that the alignement and size match Header.
+        unsafe { &*self.as_ptr().cast::<Header>() }
+    }
+}
+
+fn find_instance_img(pci_root: &mut PciRoot) -> Result<Partition> {
+    for device in VirtIOBlkIterator::new(pci_root) {
+        match Partition::get_by_name(device, "vm-instance") {
+            Ok(Some(p)) => return Ok(p),
+            Ok(None) => {}
+            Err(e) => log::warn!("error while reading from disk: {e}"),
+        };
+    }
+
+    Err(Error::MissingInstanceImage)
+}
+
+#[derive(Debug)]
+enum PvmfwEntry {
+    Existing { header_index: usize, payload_size: usize },
+    New { header_index: usize },
+}
+
+const BLK_SIZE: usize = Partitions::LBA_SIZE;
+
+impl PvmfwEntry {
+    const UUID: Uuid = Uuid::from_u128(0x90d2174a038a4bc6adf3824848fc5825);
+}
+
+fn locate_entry(partition: &mut Partition) -> Result<PvmfwEntry> {
+    let mut blk = [0; BLK_SIZE];
+    let mut indices = partition.indices();
+    let header_index = indices.next().ok_or(Error::MissingInstanceImageHeader)?;
+    partition.read_block(header_index, &mut blk).map_err(Error::FailedIo)?;
+    // The instance.img header is only used for discovery/validation.
+    let _ = Header::from_bytes(&blk).ok_or(Error::InvalidInstanceImageHeader)?;
+
+    while let Some(header_index) = indices.next() {
+        partition.read_block(header_index, &mut blk).map_err(Error::FailedIo)?;
+
+        let header: &EntryHeader = blk[..size_of::<EntryHeader>()].as_ref();
+        match (header.uuid(), header.payload_size()) {
+            (uuid, _) if uuid.is_nil() => return Ok(PvmfwEntry::New { header_index }),
+            (PvmfwEntry::UUID, payload_size) => {
+                return Ok(PvmfwEntry::Existing { header_index, payload_size })
+            }
+            (uuid, payload_size) => {
+                trace!("Skipping instance.img entry {uuid}: {payload_size:?} bytes");
+                let n = ceiling_div(payload_size, BLK_SIZE).unwrap();
+                if n > 0 {
+                    let _ = indices.nth(n - 1); // consume
+                }
+            }
+        };
+    }
+
+    Err(Error::InstanceImageFull)
+}
+
+/// Marks the start of an instance.img entry.
+///
+/// Note: Virtualization/microdroid_manager/src/instance.rs uses the name "partition".
+#[repr(C)]
+struct EntryHeader {
+    uuid: u128,
+    payload_size: u64,
+}
+
+impl EntryHeader {
+    fn new(uuid: Uuid, payload_size: usize) -> Self {
+        Self { uuid: uuid.as_u128(), payload_size: u64::try_from(payload_size).unwrap().to_le() }
+    }
+
+    fn uuid(&self) -> Uuid {
+        Uuid::from_u128(self.uuid)
+    }
+
+    fn payload_size(&self) -> usize {
+        usize::try_from(u64::from_le(self.payload_size)).unwrap()
+    }
+}
+
+impl AsRef<EntryHeader> for [u8] {
+    fn as_ref(&self) -> &EntryHeader {
+        assert_eq!(self.len(), size_of::<EntryHeader>());
+        // SAFETY - The size of the slice was checked and any value may be considered valid.
+        unsafe { &*self.as_ptr().cast::<EntryHeader>() }
+    }
+}
+
+impl AsRef<[u8]> for EntryHeader {
+    fn as_ref(&self) -> &[u8] {
+        let s = self as *const Self;
+        // SAFETY - Transmute the (valid) bytes into a slice.
+        unsafe { slice::from_raw_parts(s.cast::<u8>(), size_of::<Self>()) }
+    }
+}
+
+#[repr(C)]
+struct EntryBody {
+    code_hash: Hash,
+    auth_hash: Hash,
+    salt: Hidden,
+    mode: u8,
+}
+
+impl EntryBody {
+    fn new(dice_inputs: &PartialInputs, salt: &Hidden) -> Self {
+        let mode = match dice_inputs.mode {
+            DiceMode::kDiceModeNotInitialized => 0,
+            DiceMode::kDiceModeNormal => 1,
+            DiceMode::kDiceModeDebug => 2,
+            DiceMode::kDiceModeMaintenance => 3,
+        };
+
+        Self {
+            code_hash: dice_inputs.code_hash,
+            auth_hash: dice_inputs.auth_hash,
+            salt: *salt,
+            mode,
+        }
+    }
+
+    fn mode(&self) -> DiceMode {
+        match self.mode {
+            1 => DiceMode::kDiceModeNormal,
+            2 => DiceMode::kDiceModeDebug,
+            3 => DiceMode::kDiceModeMaintenance,
+            _ => DiceMode::kDiceModeNotInitialized,
+        }
+    }
+}
+
+impl AsRef<EntryBody> for [u8] {
+    fn as_ref(&self) -> &EntryBody {
+        assert_eq!(self.len(), size_of::<EntryBody>());
+        // SAFETY - The size of the slice was checked and members are validated by accessors.
+        unsafe { &*self.as_ptr().cast::<EntryBody>() }
+    }
+}
+
+impl AsRef<[u8]> for EntryBody {
+    fn as_ref(&self) -> &[u8] {
+        let s = self as *const Self;
+        // SAFETY - Transmute the (valid) bytes into a slice.
+        unsafe { slice::from_raw_parts(s.cast::<u8>(), size_of::<Self>()) }
+    }
+}
diff --git a/pvmfw/src/main.rs b/pvmfw/src/main.rs
index be5a16a..577ad6e 100644
--- a/pvmfw/src/main.rs
+++ b/pvmfw/src/main.rs
@@ -21,32 +21,38 @@
 extern crate alloc;
 
 mod config;
+mod crypto;
 mod debug_policy;
 mod dice;
 mod entry;
 mod exceptions;
 mod fdt;
+mod gpt;
 mod heap;
 mod helpers;
 mod hvc;
+mod hypervisor;
+mod instance;
 mod memory;
 mod mmio_guard;
 mod mmu;
+mod rand;
 mod smccc;
 mod virtio;
 
 use alloc::boxed::Box;
 
-use crate::{
-    dice::derive_next_bcc,
-    entry::RebootReason,
-    fdt::add_dice_node,
-    helpers::flush,
-    helpers::GUEST_PAGE_SIZE,
-    memory::MemoryTracker,
-    virtio::pci::{self, find_virtio_devices},
-};
-use ::dice::bcc;
+use crate::dice::PartialInputs;
+use crate::entry::RebootReason;
+use crate::fdt::modify_for_next_stage;
+use crate::helpers::flush;
+use crate::helpers::GUEST_PAGE_SIZE;
+use crate::instance::get_or_generate_instance_salt;
+use crate::memory::MemoryTracker;
+use crate::virtio::pci;
+use diced_open_dice::bcc_handover_main_flow;
+use diced_open_dice::bcc_handover_parse;
+use diced_open_dice::DiceArtifacts;
 use fdtpci::{PciError, PciInfo};
 use libfdt::Fdt;
 use log::{debug, error, info, trace};
@@ -59,11 +65,11 @@
     fdt: &mut Fdt,
     signed_kernel: &[u8],
     ramdisk: Option<&[u8]>,
-    bcc: &bcc::Handover,
+    current_bcc_handover: &[u8],
     memory: &mut MemoryTracker,
 ) -> Result<(), RebootReason> {
     info!("pVM firmware");
-    debug!("FDT: {:?}", fdt as *const libfdt::Fdt);
+    debug!("FDT: {:?}", fdt.as_ptr());
     debug!("Signed kernel: {:?} ({:#x} bytes)", signed_kernel.as_ptr(), signed_kernel.len());
     debug!("AVB public key: addr={:?}, size={:#x} ({1})", PUBLIC_KEY.as_ptr(), PUBLIC_KEY.len());
     if let Some(rd) = ramdisk {
@@ -71,13 +77,16 @@
     } else {
         debug!("Ramdisk: None");
     }
-    trace!("BCC: {bcc:x?}");
+    let bcc_handover = bcc_handover_parse(current_bcc_handover).map_err(|e| {
+        error!("Invalid BCC Handover: {e:?}");
+        RebootReason::InvalidBcc
+    })?;
+    trace!("BCC: {bcc_handover:x?}");
 
     // Set up PCI bus for VirtIO devices.
     let pci_info = PciInfo::from_fdt(fdt).map_err(handle_pci_error)?;
     debug!("PCI: {:#x?}", pci_info);
     let mut pci_root = pci::initialise(pci_info, memory)?;
-    find_virtio_devices(&mut pci_root).map_err(handle_pci_error)?;
 
     let verified_boot_data = verify_payload(signed_kernel, ramdisk, PUBLIC_KEY).map_err(|e| {
         error!("Failed to verify the payload: {e}");
@@ -90,17 +99,32 @@
     })?;
     // By leaking the slice, its content will be left behind for the next stage.
     let next_bcc = Box::leak(next_bcc);
-    let next_bcc_size =
-        derive_next_bcc(bcc, next_bcc, &verified_boot_data, PUBLIC_KEY).map_err(|e| {
-            error!("Failed to derive next-stage DICE secrets: {e:?}");
-            RebootReason::SecretDerivationError
-        })?;
-    trace!("Next BCC: {:x?}", bcc::Handover::new(&next_bcc[..next_bcc_size]));
 
+    let dice_inputs = PartialInputs::new(&verified_boot_data).map_err(|e| {
+        error!("Failed to compute partial DICE inputs: {e:?}");
+        RebootReason::InternalError
+    })?;
+    let cdi_seal = DiceArtifacts::cdi_seal(&bcc_handover);
+    let (new_instance, salt) = get_or_generate_instance_salt(&mut pci_root, &dice_inputs, cdi_seal)
+        .map_err(|e| {
+            error!("Failed to get instance.img salt: {e}");
+            RebootReason::InternalError
+        })?;
+    trace!("Got salt from instance.img: {salt:x?}");
+
+    let dice_inputs = dice_inputs.into_input_values(&salt).map_err(|e| {
+        error!("Failed to generate DICE inputs: {e:?}");
+        RebootReason::InternalError
+    })?;
+    let _ = bcc_handover_main_flow(current_bcc_handover, &dice_inputs, next_bcc).map_err(|e| {
+        error!("Failed to derive next-stage DICE secrets: {e:?}");
+        RebootReason::SecretDerivationError
+    })?;
     flush(next_bcc);
 
-    add_dice_node(fdt, next_bcc.as_ptr() as usize, NEXT_BCC_SIZE).map_err(|e| {
-        error!("Failed to add DICE node to device tree: {e}");
+    let strict_boot = true;
+    modify_for_next_stage(fdt, next_bcc, new_instance, strict_boot).map_err(|e| {
+        error!("Failed to configure device tree: {e}");
         RebootReason::InternalError
     })?;
 
diff --git a/pvmfw/src/memory.rs b/pvmfw/src/memory.rs
index 7eecb97..17dd36b 100644
--- a/pvmfw/src/memory.rs
+++ b/pvmfw/src/memory.rs
@@ -17,7 +17,7 @@
 #![deny(unsafe_op_in_unsafe_fn)]
 
 use crate::helpers::{self, align_down, align_up, page_4kb_of, SIZE_4KB};
-use crate::hvc::{hyp_meminfo, mem_share, mem_unshare};
+use crate::hypervisor::{hyp_meminfo, mem_share, mem_unshare};
 use crate::mmio_guard;
 use crate::mmu;
 use crate::smccc;
@@ -267,7 +267,7 @@
         for region in &self.regions {
             match region.mem_type {
                 MemoryType::ReadWrite => {
-                    // TODO: Use page table's dirty bit to only flush pages that were touched.
+                    // TODO(b/269738062): Use PT's dirty bit to only flush pages that were touched.
                     helpers::flush_region(region.range.start, region.range.len())
                 }
                 MemoryType::ReadOnly => {}
@@ -314,10 +314,7 @@
     // non-zero size.
     let buffer = unsafe { alloc_zeroed(layout) };
 
-    // TODO: Use let-else once we have Rust 1.65 in AOSP.
-    let buffer = if let Some(buffer) = NonNull::new(buffer) {
-        buffer
-    } else {
+    let Some(buffer) = NonNull::new(buffer) else {
         handle_alloc_error(layout);
     };
 
diff --git a/pvmfw/src/mmio_guard.rs b/pvmfw/src/mmio_guard.rs
index e5f376e..dac26e0 100644
--- a/pvmfw/src/mmio_guard.rs
+++ b/pvmfw/src/mmio_guard.rs
@@ -15,7 +15,7 @@
 //! Safe MMIO_GUARD support.
 
 use crate::helpers;
-use crate::hvc::{mmio_guard_enroll, mmio_guard_info, mmio_guard_map, mmio_guard_unmap};
+use crate::hypervisor::{mmio_guard_enroll, mmio_guard_info, mmio_guard_map, mmio_guard_unmap};
 use crate::smccc;
 use core::{fmt, result};
 
diff --git a/pvmfw/src/rand.rs b/pvmfw/src/rand.rs
new file mode 100644
index 0000000..bf0edd5
--- /dev/null
+++ b/pvmfw/src/rand.rs
@@ -0,0 +1,109 @@
+// Copyright 2023, 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.
+
+use crate::hvc;
+use core::fmt;
+use core::mem::size_of;
+
+pub enum Error {
+    /// Error during SMCCC TRNG call.
+    Trng(hvc::trng::Error),
+    /// Unsupported SMCCC TRNG version.
+    UnsupportedVersion((u16, u16)),
+}
+
+impl From<hvc::trng::Error> for Error {
+    fn from(e: hvc::trng::Error) -> Self {
+        Self::Trng(e)
+    }
+}
+
+pub type Result<T> = core::result::Result<T, Error>;
+
+impl fmt::Display for Error {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Self::Trng(e) => write!(f, "SMCCC TRNG error: {e}"),
+            Self::UnsupportedVersion((x, y)) => {
+                write!(f, "Unsupported SMCCC TRNG version v{x}.{y}")
+            }
+        }
+    }
+}
+
+/// Configure the source of entropy.
+pub fn init() -> Result<()> {
+    match hvc::trng_version()? {
+        (1, _) => Ok(()),
+        version => Err(Error::UnsupportedVersion(version)),
+    }
+}
+
+fn fill_with_entropy(s: &mut [u8]) -> Result<()> {
+    const MAX_BYTES_PER_CALL: usize = size_of::<hvc::TrngRng64Entropy>();
+
+    let (aligned, remainder) = s.split_at_mut(s.len() - s.len() % MAX_BYTES_PER_CALL);
+
+    for chunk in aligned.chunks_exact_mut(MAX_BYTES_PER_CALL) {
+        let (r, s, t) = repeat_trng_rnd(chunk.len())?;
+
+        let mut words = chunk.chunks_exact_mut(size_of::<u64>());
+        words.next().unwrap().clone_from_slice(&t.to_ne_bytes());
+        words.next().unwrap().clone_from_slice(&s.to_ne_bytes());
+        words.next().unwrap().clone_from_slice(&r.to_ne_bytes());
+    }
+
+    if !remainder.is_empty() {
+        let mut entropy = [0; MAX_BYTES_PER_CALL];
+        let (r, s, t) = repeat_trng_rnd(remainder.len())?;
+
+        let mut words = entropy.chunks_exact_mut(size_of::<u64>());
+        words.next().unwrap().clone_from_slice(&t.to_ne_bytes());
+        words.next().unwrap().clone_from_slice(&s.to_ne_bytes());
+        words.next().unwrap().clone_from_slice(&r.to_ne_bytes());
+
+        remainder.clone_from_slice(&entropy[..remainder.len()]);
+    }
+
+    Ok(())
+}
+
+fn repeat_trng_rnd(n_bytes: usize) -> hvc::trng::Result<hvc::TrngRng64Entropy> {
+    let bits = usize::try_from(u8::BITS).unwrap();
+    let n_bits = (n_bytes * bits).try_into().unwrap();
+    loop {
+        match hvc::trng_rnd64(n_bits) {
+            Err(hvc::trng::Error::NoEntropy) => continue,
+            res => return res,
+        }
+    }
+}
+
+pub fn random_array<const N: usize>() -> Result<[u8; N]> {
+    let mut arr = [0; N];
+    fill_with_entropy(&mut arr)?;
+    Ok(arr)
+}
+
+#[no_mangle]
+extern "C" fn CRYPTO_sysrand_for_seed(out: *mut u8, req: usize) {
+    CRYPTO_sysrand(out, req)
+}
+
+#[no_mangle]
+extern "C" fn CRYPTO_sysrand(out: *mut u8, req: usize) {
+    // SAFETY - We need to assume that out points to valid memory of size req.
+    let s = unsafe { core::slice::from_raw_parts_mut(out, req) };
+    let _ = fill_with_entropy(s);
+}
diff --git a/pvmfw/src/smccc.rs b/pvmfw/src/smccc.rs
index f92c076..ccf2680 100644
--- a/pvmfw/src/smccc.rs
+++ b/pvmfw/src/smccc.rs
@@ -16,7 +16,7 @@
 
 // TODO(b/245889995): use psci-0.1.1 crate
 #[inline(always)]
-fn hvc64(function: u32, args: [u64; 17]) -> [u64; 18] {
+pub fn hvc64(function: u32, args: [u64; 17]) -> [u64; 18] {
     #[cfg(target_arch = "aarch64")]
     unsafe {
         let mut ret = [0; 18];
diff --git a/pvmfw/src/virtio/pci.rs b/pvmfw/src/virtio/pci.rs
index d3b3124..58bc07e 100644
--- a/pvmfw/src/virtio/pci.rs
+++ b/pvmfw/src/virtio/pci.rs
@@ -17,13 +17,16 @@
 use super::hal::HalImpl;
 use crate::{entry::RebootReason, memory::MemoryTracker};
 use alloc::boxed::Box;
-use fdtpci::{PciError, PciInfo};
-use log::{debug, error, info};
+use fdtpci::PciInfo;
+use log::{debug, error};
 use once_cell::race::OnceBox;
 use virtio_drivers::{
-    device::blk::VirtIOBlk,
+    device::blk,
     transport::{
-        pci::{bus::PciRoot, virtio_device_type, PciTransport},
+        pci::{
+            bus::{BusDeviceIterator, PciRoot},
+            virtio_device_type, PciTransport,
+        },
         DeviceType, Transport,
     },
 };
@@ -66,31 +69,48 @@
     Ok(())
 }
 
-/// Finds VirtIO PCI devices.
-pub fn find_virtio_devices(pci_root: &mut PciRoot) -> Result<(), PciError> {
-    for (device_function, info) in pci_root.enumerate_bus(0) {
-        let (status, command) = pci_root.get_status_command(device_function);
-        debug!(
-            "Found PCI device {} at {}, status {:?} command {:?}",
-            info, device_function, status, command
-        );
-        if let Some(virtio_type) = virtio_device_type(&info) {
+pub type VirtIOBlk = blk::VirtIOBlk<HalImpl, PciTransport>;
+
+pub struct VirtIOBlkIterator<'a> {
+    pci_root: &'a mut PciRoot,
+    bus: BusDeviceIterator,
+}
+
+impl<'a> VirtIOBlkIterator<'a> {
+    pub fn new(pci_root: &'a mut PciRoot) -> Self {
+        let bus = pci_root.enumerate_bus(0);
+        Self { pci_root, bus }
+    }
+}
+
+impl<'a> Iterator for VirtIOBlkIterator<'a> {
+    type Item = VirtIOBlk;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        loop {
+            let (device_function, info) = self.bus.next()?;
+            let (status, command) = self.pci_root.get_status_command(device_function);
+            debug!(
+                "Found PCI device {} at {}, status {:?} command {:?}",
+                info, device_function, status, command
+            );
+
+            let Some(virtio_type) = virtio_device_type(&info) else {
+                continue;
+            };
             debug!("  VirtIO {:?}", virtio_type);
-            let mut transport = PciTransport::new::<HalImpl>(pci_root, device_function).unwrap();
-            info!(
+
+            let mut transport =
+                PciTransport::new::<HalImpl>(self.pci_root, device_function).unwrap();
+            debug!(
                 "Detected virtio PCI device with device type {:?}, features {:#018x}",
                 transport.device_type(),
                 transport.read_device_features(),
             );
+
             if virtio_type == DeviceType::Block {
-                let mut blk =
-                    VirtIOBlk::<HalImpl, _>::new(transport).expect("failed to create blk driver");
-                info!("Found {} KiB block device.", blk.capacity() * 512 / 1024);
-                let mut data = [0; 512];
-                blk.read_block(0, &mut data).expect("Failed to read block device");
+                return Some(Self::Item::new(transport).expect("failed to create blk driver"));
             }
         }
     }
-
-    Ok(())
 }
diff --git a/rialto/tests/test.rs b/rialto/tests/test.rs
index b25034f..be5f118 100644
--- a/rialto/tests/test.rs
+++ b/rialto/tests/test.rs
@@ -16,7 +16,7 @@
 
 use android_system_virtualizationservice::{
     aidl::android::system::virtualizationservice::{
-        VirtualMachineConfig::VirtualMachineConfig,
+        CpuTopology::CpuTopology, VirtualMachineConfig::VirtualMachineConfig,
         VirtualMachineRawConfig::VirtualMachineRawConfig,
     },
     binder::{ParcelFileDescriptor, ProcessState},
@@ -65,9 +65,10 @@
         disks: vec![],
         protectedVm: false,
         memoryMib: 300,
-        numCpus: 1,
+        cpuTopology: CpuTopology::ONE_CPU,
         platformVersion: "~1.0".to_string(),
         taskProfiles: vec![],
+        gdbPort: 0, // No gdb
     });
     let vm = VmInstance::create(service.as_ref(), &config, Some(console), Some(log), None)
         .context("Failed to create VM")?;
diff --git a/tests/aidl/Android.bp b/tests/aidl/Android.bp
index d59ca7e..ed4e8ff 100644
--- a/tests/aidl/Android.bp
+++ b/tests/aidl/Android.bp
@@ -6,6 +6,10 @@
     name: "com.android.microdroid.testservice",
     srcs: ["com/android/microdroid/testservice/**/*.aidl"],
     unstable: true,
+    flags: [
+        "-Werror",
+        "-Wno-mixed-oneway",
+    ],
     backend: {
         java: {
             gen_rpc: true,
diff --git a/tests/aidl/com/android/microdroid/testservice/IAppCallback.aidl b/tests/aidl/com/android/microdroid/testservice/IAppCallback.aidl
new file mode 100644
index 0000000..9859090
--- /dev/null
+++ b/tests/aidl/com/android/microdroid/testservice/IAppCallback.aidl
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2023 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.microdroid.testservice;
+
+import com.android.microdroid.testservice.IVmCallback;
+
+/**
+ * An interface exposed by the app for callbacks from the VM.
+ *
+ * {@hide}
+ */
+interface IAppCallback {
+    /** Invites the app to call vmCallback#echoMessage() */
+    void setVmCallback(IVmCallback vmCallback);
+
+    /** Asynchronusly called by the VM in response to a call to echoMessage(). */
+    void onEchoRequestReceived(String message);
+}
diff --git a/tests/aidl/com/android/microdroid/testservice/ITestService.aidl b/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
index 4611134..36c3aaf 100644
--- a/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
+++ b/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
@@ -15,7 +15,12 @@
  */
 package com.android.microdroid.testservice;
 
-/** {@hide} */
+import com.android.microdroid.testservice.IAppCallback;
+
+/**
+ * This is the service exposed by the test payload, called by the test app.
+ * {@hide}
+ */
 interface ITestService {
     const long SERVICE_PORT = 5678;
 
@@ -56,6 +61,15 @@
     /* get the content of the specified file. */
     String readFromFile(String path);
 
+    /* get file permissions of the give file by stat'ing it */
+    int getFilePermissions(String path);
+
+    /** Returns flags for the given mountPoint. */
+    int getMountFlags(String mountPoint);
+
+    /** Requests the VM to asynchronously call appCallback.setVmCallback() */
+    void requestCallback(IAppCallback appCallback);
+
     /**
      * Request the service to exit, triggering the termination of the VM. This may cause any
      * requests in flight to fail.
diff --git a/tests/aidl/com/android/microdroid/testservice/IVmCallback.aidl b/tests/aidl/com/android/microdroid/testservice/IVmCallback.aidl
new file mode 100644
index 0000000..617d184
--- /dev/null
+++ b/tests/aidl/com/android/microdroid/testservice/IVmCallback.aidl
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2023 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.microdroid.testservice;
+
+/**
+ * An interface exposed by the VM for callbacks from the app.
+ *
+ * {@hide}
+ */
+interface IVmCallback {
+    /** Requests the VM to asynchronously call the app's onEchoRequestReceived() callback. */
+    void echoMessage(String message);
+}
diff --git a/tests/benchmark/Android.bp b/tests/benchmark/Android.bp
index 9d2b6c7..dac4993 100644
--- a/tests/benchmark/Android.bp
+++ b/tests/benchmark/Android.bp
@@ -19,6 +19,7 @@
     jni_libs: [
         "MicrodroidBenchmarkNativeLib",
         "MicrodroidIdleNativeLib",
+        "MicrodroidTestNativeLib",
         "libiovsock_host_jni",
     ],
     jni_uses_platform_apis: true,
diff --git a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
index f17000a..4b11d77 100644
--- a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
+++ b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
@@ -16,6 +16,8 @@
 
 package com.android.microdroid.benchmark;
 
+import static android.system.virtualmachine.VirtualMachineConfig.CPU_TOPOLOGY_ONE_CPU;
+import static android.system.virtualmachine.VirtualMachineConfig.CPU_TOPOLOGY_MATCH_HOST;
 import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_FULL;
 import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_NONE;
 
@@ -27,6 +29,8 @@
 import android.app.Instrumentation;
 import android.os.Bundle;
 import android.os.ParcelFileDescriptor;
+import android.os.ParcelFileDescriptor.AutoCloseInputStream;
+import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
 import android.os.Process;
 import android.os.RemoteException;
 import android.system.virtualmachine.VirtualMachine;
@@ -38,6 +42,7 @@
 import com.android.microdroid.test.common.ProcessUtil;
 import com.android.microdroid.test.device.MicrodroidDeviceTestBase;
 import com.android.microdroid.testservice.IBenchmarkService;
+import com.android.microdroid.testservice.ITestService;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -46,12 +51,21 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 
+import java.io.BufferedReader;
 import java.io.File;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
 import java.nio.file.Files;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.OptionalLong;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Function;
 
@@ -66,7 +80,8 @@
 
     private static final String APEX_ETC_FS = "/apex/com.android.virt/etc/fs/";
     private static final double SIZE_MB = 1024.0 * 1024.0;
-    private static final double NANO_TO_MILLI = 1000000.0;
+    private static final double NANO_TO_MILLI = 1_000_000.0;
+    private static final double NANO_TO_MICRO = 1_000.0;
     private static final String MICRODROID_IMG_PREFIX = "microdroid_";
     private static final String MICRODROID_IMG_SUFFIX = ".img";
 
@@ -136,102 +151,87 @@
         mInstrumentation.sendStatus(0, bundle);
     }
 
-    @Test
-    public void testMicrodroidBootTime()
+    private static class BootTimeStats {
+        private final Map<BootTimeMetric, List<Double>> mData = new HashMap<>();
+
+        public BootTimeStats(int trialCount) {
+            for (BootTimeMetric metric : BootTimeMetric.values()) {
+                mData.put(metric, new ArrayList<>(trialCount));
+            }
+        }
+
+        public void collect(BootResult result) {
+            for (BootTimeMetric metric : BootTimeMetric.values()) {
+                OptionalLong value = result.getBootTimeMetricNanoTime(metric);
+                if (value.isPresent()) {
+                    mData.get(metric).add(value.getAsLong() / NANO_TO_MILLI);
+                }
+            }
+        }
+
+        public List<Double> get(BootTimeMetric metric) {
+            return Collections.unmodifiableList(mData.get(metric));
+        }
+    }
+
+    private BootTimeStats runBootTimeTest(
+            String name,
+            Function<VirtualMachineConfig.Builder, VirtualMachineConfig.Builder> fnConfig)
             throws VirtualMachineException, InterruptedException, IOException {
         assume().withMessage("Skip on CF; too slow").that(isCuttlefish()).isFalse();
 
         final int trialCount = 10;
 
-        List<Double> bootTimeMetrics = new ArrayList<>();
+        BootTimeStats stats = new BootTimeStats(trialCount);
         for (int i = 0; i < trialCount; i++) {
-            VirtualMachineConfig normalConfig =
+            VirtualMachineConfig.Builder builder =
                     newVmConfigBuilder()
                             .setPayloadBinaryName("MicrodroidIdleNativeLib.so")
-                            .setDebugLevel(DEBUG_LEVEL_NONE)
                             .setMemoryBytes(256 * ONE_MEBI)
-                            .build();
-            forceCreateNewVirtualMachine("test_vm_boot_time", normalConfig);
+                            .setDebugLevel(DEBUG_LEVEL_NONE);
+            VirtualMachineConfig config = fnConfig.apply(builder).build();
+            forceCreateNewVirtualMachine(name, config);
 
-            BootResult result = tryBootVm(TAG, "test_vm_boot_time");
+            BootResult result = tryBootVm(TAG, name);
             assertThat(result.payloadStarted).isTrue();
-
-            bootTimeMetrics.add(result.endToEndNanoTime / NANO_TO_MILLI);
+            stats.collect(result);
         }
-
-        reportMetrics(bootTimeMetrics, "boot_time", "ms");
+        return stats;
     }
 
     @Test
-    public void testMicrodroidMulticoreBootTime()
+    public void testMicrodroidBootTime()
             throws VirtualMachineException, InterruptedException, IOException {
-        assume().withMessage("Skip on CF; too slow").that(isCuttlefish()).isFalse();
+        BootTimeStats stats =
+                runBootTimeTest(
+                        "test_vm_boot_time",
+                        (builder) -> builder.setCpuTopology(CPU_TOPOLOGY_ONE_CPU));
+        reportMetrics(stats.get(BootTimeMetric.TOTAL), "boot_time", "ms");
+    }
 
-        final int trialCount = 10;
-        final int[] trialNumCpus = {2, 4, 8};
-
-        for (int numCpus : trialNumCpus) {
-            List<Double> bootTimeMetrics = new ArrayList<>();
-            for (int i = 0; i < trialCount; i++) {
-                VirtualMachineConfig normalConfig =
-                        newVmConfigBuilder()
-                                .setPayloadBinaryName("MicrodroidIdleNativeLib.so")
-                                .setDebugLevel(DEBUG_LEVEL_NONE)
-                                .setMemoryBytes(256 * ONE_MEBI)
-                                .setNumCpus(numCpus)
-                                .build();
-                forceCreateNewVirtualMachine("test_vm_boot_time_multicore", normalConfig);
-
-                BootResult result = tryBootVm(TAG, "test_vm_boot_time_multicore");
-                assertThat(result.payloadStarted).isTrue();
-
-                bootTimeMetrics.add(result.endToEndNanoTime / NANO_TO_MILLI);
-            }
-
-            String metricName = "boot_time_" + numCpus + "cpus";
-            reportMetrics(bootTimeMetrics, metricName, "ms");
-        }
+    @Test
+    public void testMicrodroidHostCpuTopologyBootTime()
+            throws VirtualMachineException, InterruptedException, IOException {
+        BootTimeStats stats =
+                runBootTimeTest(
+                        "test_vm_boot_time_host_topology",
+                        (builder) -> builder.setCpuTopology(CPU_TOPOLOGY_MATCH_HOST));
+        reportMetrics(stats.get(BootTimeMetric.TOTAL), "boot_time", "ms");
     }
 
     @Test
     public void testMicrodroidDebugBootTime()
             throws VirtualMachineException, InterruptedException, IOException {
-        assume().withMessage("Skip on CF; too slow").that(isCuttlefish()).isFalse();
-
-        final int trialCount = 10;
-
-        List<Double> vmStartingTimeMetrics = new ArrayList<>();
-        List<Double> bootTimeMetrics = new ArrayList<>();
-        List<Double> bootloaderTimeMetrics = new ArrayList<>();
-        List<Double> kernelBootTimeMetrics = new ArrayList<>();
-        List<Double> userspaceBootTimeMetrics = new ArrayList<>();
-
-        for (int i = 0; i < trialCount; i++) {
-            // To grab boot events from log, set debug mode to FULL
-            VirtualMachineConfig normalConfig =
-                    newVmConfigBuilder()
-                            .setPayloadBinaryName("MicrodroidIdleNativeLib.so")
-                            .setDebugLevel(DEBUG_LEVEL_FULL)
-                            .setVmOutputCaptured(true)
-                            .setMemoryBytes(256 * ONE_MEBI)
-                            .build();
-            forceCreateNewVirtualMachine("test_vm_boot_time_debug", normalConfig);
-
-            BootResult result = tryBootVm(TAG, "test_vm_boot_time_debug");
-            assertThat(result.payloadStarted).isTrue();
-
-            vmStartingTimeMetrics.add(result.getVMStartingElapsedNanoTime() / NANO_TO_MILLI);
-            bootTimeMetrics.add(result.endToEndNanoTime / NANO_TO_MILLI);
-            bootloaderTimeMetrics.add(result.getBootloaderElapsedNanoTime() / NANO_TO_MILLI);
-            kernelBootTimeMetrics.add(result.getKernelElapsedNanoTime() / NANO_TO_MILLI);
-            userspaceBootTimeMetrics.add(result.getUserspaceElapsedNanoTime() / NANO_TO_MILLI);
-        }
-
-        reportMetrics(vmStartingTimeMetrics, "vm_starting_time", "ms");
-        reportMetrics(bootTimeMetrics, "boot_time", "ms");
-        reportMetrics(bootloaderTimeMetrics, "bootloader_time", "ms");
-        reportMetrics(kernelBootTimeMetrics, "kernel_boot_time", "ms");
-        reportMetrics(userspaceBootTimeMetrics, "userspace_boot_time", "ms");
+        BootTimeStats stats =
+                runBootTimeTest(
+                        "test_vm_boot_time_debug",
+                        (builder) ->
+                                builder.setDebugLevel(DEBUG_LEVEL_FULL).setVmOutputCaptured(true));
+        reportMetrics(stats.get(BootTimeMetric.TOTAL), "boot_time", "ms");
+        reportMetrics(stats.get(BootTimeMetric.VM_START), "vm_starting_time", "ms");
+        reportMetrics(stats.get(BootTimeMetric.BOOTLOADER), "bootloader_time", "ms");
+        reportMetrics(stats.get(BootTimeMetric.KERNEL), "kernel_boot_time", "ms");
+        reportMetrics(stats.get(BootTimeMetric.USERSPACE), "userspace_boot_time", "ms");
     }
 
     @Test
@@ -260,7 +260,7 @@
         VirtualMachineConfig config =
                 newVmConfigBuilder()
                         .setPayloadConfigPath("assets/vm_config_io.json")
-                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .setDebugLevel(DEBUG_LEVEL_NONE)
                         .build();
         List<Double> transferRates = new ArrayList<>(IO_TEST_TRIAL_COUNT);
 
@@ -287,7 +287,7 @@
         VirtualMachineConfig config =
                 newVmConfigBuilder()
                         .setPayloadConfigPath("assets/vm_config_io.json")
-                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .setDebugLevel(DEBUG_LEVEL_NONE)
                         .build();
         List<Double> readRates = new ArrayList<>(IO_TEST_TRIAL_COUNT);
 
@@ -576,4 +576,115 @@
             }
         }
     }
+
+    @Test
+    public void testRpcBinderLatency() throws Exception {
+        final int NUM_WARMUPS = 10;
+        final int NUM_REQUESTS = 10_000;
+
+        VirtualMachineConfig config =
+                newVmConfigBuilder()
+                        .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+                        .setDebugLevel(DEBUG_LEVEL_NONE)
+                        .build();
+
+        List<Double> requestLatencies = new ArrayList<>(IO_TEST_TRIAL_COUNT * NUM_REQUESTS);
+        for (int i = 0; i < IO_TEST_TRIAL_COUNT; ++i) {
+            VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_latency" + i, config);
+            TestResults testResults =
+                    runVmTestService(
+                            TAG,
+                            vm,
+                            (ts, tr) -> {
+                                // Correctness check
+                                tr.mAddInteger = ts.addInteger(123, 456);
+
+                                // Warmup
+                                for (int j = 0; j < NUM_WARMUPS; j++) {
+                                    ts.addInteger(j, j + 1);
+                                }
+
+                                // Count Fibonacci numbers, measure latency.
+                                int a = 0;
+                                int b = 1;
+                                int c;
+                                tr.mTimings = new long[NUM_REQUESTS];
+                                for (int j = 0; j < NUM_REQUESTS; j++) {
+                                    long start = System.nanoTime();
+                                    c = ts.addInteger(a, b);
+                                    tr.mTimings[j] = System.nanoTime() - start;
+                                    a = b;
+                                    b = c;
+                                }
+                            });
+            testResults.assertNoException();
+            assertThat(testResults.mAddInteger).isEqualTo(579);
+            for (long duration : testResults.mTimings) {
+                requestLatencies.add((double) duration / NANO_TO_MICRO);
+            }
+        }
+        reportMetrics(requestLatencies, "latency/rpcbinder", "us");
+    }
+
+    @Test
+    public void testVsockLatency() throws Exception {
+        final int NUM_WARMUPS = 10;
+        final int NUM_REQUESTS = 10_000;
+
+        VirtualMachineConfig config =
+                newVmConfigBuilder()
+                        .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+                        .setDebugLevel(DEBUG_LEVEL_NONE)
+                        .build();
+
+        List<Double> requestLatencies = new ArrayList<>(IO_TEST_TRIAL_COUNT * NUM_REQUESTS);
+        for (int i = 0; i < IO_TEST_TRIAL_COUNT; ++i) {
+            VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_latency" + i, config);
+            TestResults testResults =
+                    runVmTestService(
+                            TAG,
+                            vm,
+                            (ts, tr) -> {
+                                ts.runEchoReverseServer();
+                                ParcelFileDescriptor pfd =
+                                        vm.connectVsock(ITestService.ECHO_REVERSE_PORT);
+                                try (InputStream input = new AutoCloseInputStream(pfd);
+                                        OutputStream output = new AutoCloseOutputStream(pfd)) {
+                                    BufferedReader reader =
+                                            new BufferedReader(new InputStreamReader(input));
+                                    Writer writer = new OutputStreamWriter(output);
+
+                                    // Correctness check.
+                                    writer.write("hello\n");
+                                    writer.flush();
+                                    tr.mFileContent = reader.readLine().trim();
+
+                                    // Warmup.
+                                    for (int j = 0; j < NUM_WARMUPS; ++j) {
+                                        String text = "test" + j + "\n";
+                                        writer.write(text);
+                                        writer.flush();
+                                        reader.readLine();
+                                    }
+
+                                    // Measured requests.
+                                    tr.mTimings = new long[NUM_REQUESTS];
+                                    for (int j = 0; j < NUM_REQUESTS; j++) {
+                                        String text = "test" + j + "\n";
+                                        long start = System.nanoTime();
+                                        writer.write(text);
+                                        writer.flush();
+                                        reader.readLine();
+                                        tr.mTimings[j] = System.nanoTime() - start;
+                                    }
+                                }
+                            });
+            testResults.assertNoException();
+            assertThat(testResults.mFileContent).isEqualTo("olleh");
+            for (long duration : testResults.mTimings) {
+                requestLatencies.add((double) duration / NANO_TO_MICRO);
+            }
+        }
+        reportMetrics(requestLatencies, "latency/vsock", "us");
+    }
 }
diff --git a/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java b/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java
index c47e915..9c8714f 100644
--- a/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java
+++ b/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java
@@ -79,14 +79,16 @@
     private static final int ROUND_IGNORE_STARTUP_TIME = 3;
     private static final String APK_NAME = "MicrodroidTestApp.apk";
     private static final String PACKAGE_NAME = "com.android.microdroid.test";
-    private static final int NUM_VCPUS = 3;
 
     private MetricsProcessor mMetricsProcessor;
     @Rule public TestMetrics mMetrics = new TestMetrics();
 
+    private boolean mNeedTearDown = false;
+
     @Before
     public void setUp() throws Exception {
         testIfDeviceIsCapable(getDevice());
+        mNeedTearDown = true;
 
         getDevice().installPackage(findTestFile(APK_NAME), /* reinstall */ false);
 
@@ -95,6 +97,12 @@
 
     @After
     public void tearDown() throws Exception {
+        if (!mNeedTearDown) {
+            // If we skipped setUp, we don't need to undo it, and that avoids potential exceptions
+            // incompatible hardware. (Note that tests can change what testIfDeviceIsCapable()
+            // sees, so we can't rely on that - b/268688303.)
+            return;
+        }
         // Set PKVM enable and reboot to prevent previous staged session.
         if (!isCuttlefish()) {
             setPKVMStatusWithRebootToBootloader(true);
@@ -247,7 +255,7 @@
                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
                         .debugLevel("full")
                         .memoryMib(vm_mem_mb)
-                        .numCpus(NUM_VCPUS)
+                        .cpuTopology("match_host")
                         .build(device);
         microdroidDevice.waitForBootComplete(30000);
         microdroidDevice.enableAdbRoot();
diff --git a/tests/helper/Android.bp b/tests/helper/Android.bp
index 61c5dcd..c9eafad 100644
--- a/tests/helper/Android.bp
+++ b/tests/helper/Android.bp
@@ -15,6 +15,7 @@
     static_libs: [
         "androidx.test.runner",
         "androidx.test.ext.junit",
+        "com.android.microdroid.testservice-java",
         "MicrodroidTestHelper",
         "truth-prebuilt",
     ],
diff --git a/tests/helper/src/java/com/android/microdroid/test/common/DeviceProperties.java b/tests/helper/src/java/com/android/microdroid/test/common/DeviceProperties.java
index 94f7e99..23f8ca6 100644
--- a/tests/helper/src/java/com/android/microdroid/test/common/DeviceProperties.java
+++ b/tests/helper/src/java/com/android/microdroid/test/common/DeviceProperties.java
@@ -20,15 +20,18 @@
 
 /** This class can be used in both host tests and device tests to get the device properties. */
 public final class DeviceProperties {
+
     /** PropertyGetter is used to get the property associated to a given key. */
     public interface PropertyGetter {
         String getProperty(String key) throws Exception;
     }
 
     private static final String KEY_VENDOR_DEVICE = "ro.product.vendor.device";
+    private static final String KEY_BUILD_TYPE = "ro.build.type";
     private static final String KEY_METRICS_TAG = "debug.hypervisor.metrics_tag";
 
     private static final String CUTTLEFISH_DEVICE_PREFIX = "vsoc_";
+    private static final String USER_BUILD_TYPE = "user";
 
     private final PropertyGetter mPropertyGetter;
 
@@ -49,6 +52,13 @@
         return vendorDeviceName != null && vendorDeviceName.startsWith(CUTTLEFISH_DEVICE_PREFIX);
     }
 
+    /**
+     * @return whether the device is user build.
+     */
+    public boolean isUserBuild() {
+        return USER_BUILD_TYPE.equals(getProperty(KEY_BUILD_TYPE));
+    }
+
     public String getMetricsTag() {
         return getProperty(KEY_METRICS_TAG);
     }
diff --git a/tests/helper/src/java/com/android/microdroid/test/common/MetricsProcessor.java b/tests/helper/src/java/com/android/microdroid/test/common/MetricsProcessor.java
index b6bc479..42eb6a1 100644
--- a/tests/helper/src/java/com/android/microdroid/test/common/MetricsProcessor.java
+++ b/tests/helper/src/java/com/android/microdroid/test/common/MetricsProcessor.java
@@ -16,6 +16,8 @@
 
 package com.android.microdroid.test.common;
 
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -41,29 +43,42 @@
      */
     public Map<String, Double> computeStats(List<? extends Number> metrics, String name,
             String unit) {
+        List<Double> values = new ArrayList<>(metrics.size());
+        for (Number metric : metrics) {
+            values.add(metric.doubleValue());
+        }
+        Collections.sort(values);
+
         double sum = 0;
         double min = Double.MAX_VALUE;
         double max = Double.MIN_VALUE;
-        for (Number metric : metrics) {
-            double d = metric.doubleValue();
+        for (Double d : values) {
             sum += d;
             if (min > d) min = d;
             if (max < d) max = d;
         }
-        double avg = sum / metrics.size();
+        double avg = sum / values.size();
         double sqSum = 0;
-        for (Number metric : metrics) {
-            double d = metric.doubleValue();
+        for (Double d : values) {
             sqSum += (d - avg) * (d - avg);
         }
-        double stdDev = Math.sqrt(sqSum / (metrics.size() - 1));
-
+        double stdDev = Math.sqrt(sqSum / (values.size() - 1));
+        double median = Double.MIN_VALUE;
+        if (values.size() > 0) {
+            int rank = values.size() / 2;
+            if (values.size() % 2 == 0) {
+                median = (values.get(rank - 1) + values.get(rank)) / 2;
+            } else {
+                median = values.get(rank);
+            }
+        }
         Map<String, Double> stats = new HashMap<String, Double>();
         String prefix = mPrefix + name;
         stats.put(prefix + "_min_" + unit, min);
         stats.put(prefix + "_max_" + unit, max);
         stats.put(prefix + "_average_" + unit, avg);
         stats.put(prefix + "_stdev_" + unit, stdDev);
+        stats.put(prefix + "_median_" + unit, median);
         return stats;
     }
 }
diff --git a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
index f1da43a..744f94c 100644
--- a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
+++ b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
@@ -17,6 +17,7 @@
 
 import static android.content.pm.PackageManager.FEATURE_VIRTUALIZATION_FRAMEWORK;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
 
 import android.app.Instrumentation;
@@ -38,6 +39,7 @@
 
 import com.android.microdroid.test.common.DeviceProperties;
 import com.android.microdroid.test.common.MetricsProcessor;
+import com.android.microdroid.testservice.ITestService;
 
 import java.io.BufferedReader;
 import java.io.ByteArrayOutputStream;
@@ -51,15 +53,23 @@
 import java.util.concurrent.TimeUnit;
 
 public abstract class MicrodroidDeviceTestBase {
+    private static final String TAG = "MicrodroidDeviceTestBase";
     private final String MAX_PERFORMANCE_TASK_PROFILE = "CPUSET_SP_TOP_APP";
 
     public static boolean isCuttlefish() {
-        return DeviceProperties.create(SystemProperties::get).isCuttlefish();
+        return getDeviceProperties().isCuttlefish();
+    }
+
+    public static boolean isUserBuild() {
+        return getDeviceProperties().isUserBuild();
     }
 
     public static String getMetricPrefix() {
-        return MetricsProcessor.getMetricPrefix(
-                DeviceProperties.create(SystemProperties::get).getMetricsTag());
+        return MetricsProcessor.getMetricPrefix(getDeviceProperties().getMetricsTag());
+    }
+
+    private static DeviceProperties getDeviceProperties() {
+        return DeviceProperties.create(SystemProperties::get);
     }
 
     protected final void grantPermission(String permission) {
@@ -80,13 +90,22 @@
         Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
         UiAutomation uiAutomation = instrumentation.getUiAutomation();
         String cmd = "settaskprofile " + Os.gettid() + " " + MAX_PERFORMANCE_TASK_PROFILE;
-        String out = runInShell("MicrodroidDeviceTestBase", uiAutomation, cmd).trim();
+        String out = runInShell(TAG, uiAutomation, cmd).trim();
         String expect = "Profile " + MAX_PERFORMANCE_TASK_PROFILE + " is applied successfully!";
         if (!expect.equals(out)) {
             throw new IOException("Could not apply max performance task profile: " + out);
         }
     }
 
+    public final boolean getDebugPolicyBoolean(String debugPolicy) throws IOException {
+        Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+        UiAutomation uiAutomation = instrumentation.getUiAutomation();
+        String debugPolicyFilePath = "/proc/device-tree" + debugPolicy;
+        String cmd = "su root xxd -p " + debugPolicyFilePath;
+        String dp = runInShell(TAG, uiAutomation, cmd).trim();
+        return "00000001".equals(dp);
+    }
+
     private Context mCtx;
     private boolean mProtectedVm;
 
@@ -148,8 +167,9 @@
         private OptionalLong mPayloadStartedNanoTime = OptionalLong.empty();
         private StringBuilder mConsoleOutput = new StringBuilder();
         private StringBuilder mLogOutput = new StringBuilder();
+        private boolean mProcessedBootTimeMetrics = false;
 
-        private void processBootEvents(String log) {
+        private void processBootTimeMetrics(String log) {
             if (!mVcpuStartedNanoTime.isPresent()) {
                 mVcpuStartedNanoTime = OptionalLong.of(System.nanoTime());
             }
@@ -165,12 +185,13 @@
             }
         }
 
-        private void logVmOutputAndMonitorBootEvents(
+        private void logVmOutputAndMonitorBootTimeMetrics(
                 String tag,
                 InputStream vmOutputStream,
                 String name,
                 StringBuilder result,
                 boolean monitorEvents) {
+            mProcessedBootTimeMetrics |= monitorEvents;
             new Thread(
                             () -> {
                                 try {
@@ -180,7 +201,7 @@
                                     String line;
                                     while ((line = reader.readLine()) != null
                                             && !Thread.interrupted()) {
-                                        if (monitorEvents) processBootEvents(line);
+                                        if (monitorEvents) processBootTimeMetrics(line);
                                         Log.i(tag, name + ": " + line);
                                         result.append(line + "\n");
                                     }
@@ -191,15 +212,15 @@
                     .start();
         }
 
-        private void logVmOutputAndMonitorBootEvents(
+        private void logVmOutputAndMonitorBootTimeMetrics(
                 String tag, InputStream vmOutputStream, String name, StringBuilder result) {
-            logVmOutputAndMonitorBootEvents(tag, vmOutputStream, name, result, true);
+            logVmOutputAndMonitorBootTimeMetrics(tag, vmOutputStream, name, result, true);
         }
 
         /** Copy output from the VM to logcat. This is helpful when things go wrong. */
         protected void logVmOutput(
                 String tag, InputStream vmOutputStream, String name, StringBuilder result) {
-            logVmOutputAndMonitorBootEvents(tag, vmOutputStream, name, result, false);
+            logVmOutputAndMonitorBootTimeMetrics(tag, vmOutputStream, name, result, false);
         }
 
         public void runToFinish(String logTag, VirtualMachine vm)
@@ -207,7 +228,7 @@
             vm.setCallback(mExecutorService, this);
             vm.run();
             if (vm.getConfig().isVmOutputCaptured()) {
-                logVmOutputAndMonitorBootEvents(
+                logVmOutputAndMonitorBootTimeMetrics(
                         logTag, vm.getConsoleOutput(), "Console", mConsoleOutput);
                 logVmOutput(logTag, vm.getLogOutput(), "Log", mLogOutput);
             }
@@ -238,6 +259,10 @@
             return mLogOutput.toString();
         }
 
+        public boolean hasProcessedBootTimeMetrics() {
+            return mProcessedBootTimeMetrics;
+        }
+
         protected void forceStop(VirtualMachine vm) {
             try {
                 vm.stop();
@@ -266,12 +291,21 @@
         }
     }
 
+    public enum BootTimeMetric {
+        TOTAL,
+        VM_START,
+        BOOTLOADER,
+        KERNEL,
+        USERSPACE,
+    }
+
     public static class BootResult {
         public final boolean payloadStarted;
         public final int deathReason;
         public final long apiCallNanoTime;
         public final long endToEndNanoTime;
 
+        public final boolean processedBootTimeMetrics;
         public final OptionalLong vcpuStartedNanoTime;
         public final OptionalLong kernelStartedNanoTime;
         public final OptionalLong initStartedNanoTime;
@@ -285,6 +319,7 @@
                 int deathReason,
                 long apiCallNanoTime,
                 long endToEndNanoTime,
+                boolean processedBootTimeMetrics,
                 OptionalLong vcpuStartedNanoTime,
                 OptionalLong kernelStartedNanoTime,
                 OptionalLong initStartedNanoTime,
@@ -295,6 +330,7 @@
             this.payloadStarted = payloadStarted;
             this.deathReason = deathReason;
             this.endToEndNanoTime = endToEndNanoTime;
+            this.processedBootTimeMetrics = processedBootTimeMetrics;
             this.vcpuStartedNanoTime = vcpuStartedNanoTime;
             this.kernelStartedNanoTime = kernelStartedNanoTime;
             this.initStartedNanoTime = initStartedNanoTime;
@@ -336,6 +372,27 @@
         public long getUserspaceElapsedNanoTime() {
             return getPayloadStartedNanoTime() - getInitStartedNanoTime();
         }
+
+        public OptionalLong getBootTimeMetricNanoTime(BootTimeMetric metric) {
+            if (metric == BootTimeMetric.TOTAL) {
+                return OptionalLong.of(endToEndNanoTime);
+            }
+
+            if (processedBootTimeMetrics) {
+                switch (metric) {
+                    case VM_START:
+                        return OptionalLong.of(getVMStartingElapsedNanoTime());
+                    case BOOTLOADER:
+                        return OptionalLong.of(getBootloaderElapsedNanoTime());
+                    case KERNEL:
+                        return OptionalLong.of(getKernelElapsedNanoTime());
+                    case USERSPACE:
+                        return OptionalLong.of(getUserspaceElapsedNanoTime());
+                }
+            }
+
+            return OptionalLong.empty();
+        }
     }
 
     public BootResult tryBootVm(String logTag, String vmName)
@@ -366,6 +423,7 @@
                 deathReason.getNow(VmEventListener.STOP_REASON_INFRASTRUCTURE_ERROR),
                 apiCallNanoTime,
                 endTime.getNow(apiCallNanoTime) - apiCallNanoTime,
+                listener.hasProcessedBootTimeMetrics(),
                 listener.getVcpuStartedNanoTime(),
                 listener.getKernelStartedNanoTime(),
                 listener.getInitStartedNanoTime(),
@@ -389,4 +447,106 @@
             throw new RuntimeException("Failed to run the command.");
         }
     }
+
+    protected static class TestResults {
+        public Exception mException;
+        public Integer mAddInteger;
+        public String mAppRunProp;
+        public String mSublibRunProp;
+        public String mExtraApkTestProp;
+        public String mApkContentsPath;
+        public String mEncryptedStoragePath;
+        public String[] mEffectiveCapabilities;
+        public String mFileContent;
+        public byte[] mBcc;
+        public long[] mTimings;
+        public int mFileMode;
+        public int mMountFlags;
+
+        public void assertNoException() {
+            if (mException != null) {
+                // Rethrow, wrapped in a new exception, so we get stack traces of the original
+                // failure as well as the body of the test.
+                throw new RuntimeException(mException);
+            }
+        }
+    }
+
+    protected TestResults runVmTestService(
+            String logTag, VirtualMachine vm, RunTestsAgainstTestService testsToRun)
+            throws Exception {
+        CompletableFuture<Boolean> payloadStarted = new CompletableFuture<>();
+        CompletableFuture<Boolean> payloadReady = new CompletableFuture<>();
+        CompletableFuture<Boolean> payloadFinished = new CompletableFuture<>();
+        TestResults testResults = new TestResults();
+        VmEventListener listener =
+                new VmEventListener() {
+                    ITestService mTestService = null;
+
+                    private void initializeTestService(VirtualMachine vm) {
+                        try {
+                            mTestService =
+                                    ITestService.Stub.asInterface(
+                                            vm.connectToVsockServer(ITestService.SERVICE_PORT));
+                            // Make sure linkToDeath works, and include it in the log in case it's
+                            // helpful.
+                            mTestService
+                                    .asBinder()
+                                    .linkToDeath(
+                                            () -> Log.i(logTag, "ITestService binder died"), 0);
+                        } catch (Exception e) {
+                            testResults.mException = e;
+                        }
+                    }
+
+                    private void testVMService(VirtualMachine vm) {
+                        try {
+                            if (mTestService == null) initializeTestService(vm);
+                            testsToRun.runTests(mTestService, testResults);
+                        } catch (Exception e) {
+                            testResults.mException = e;
+                        }
+                    }
+
+                    private void quitVMService() {
+                        try {
+                            mTestService.quit();
+                        } catch (Exception e) {
+                            testResults.mException = e;
+                        }
+                    }
+
+                    @Override
+                    public void onPayloadReady(VirtualMachine vm) {
+                        Log.i(logTag, "onPayloadReady");
+                        payloadReady.complete(true);
+                        testVMService(vm);
+                        quitVMService();
+                    }
+
+                    @Override
+                    public void onPayloadStarted(VirtualMachine vm) {
+                        Log.i(logTag, "onPayloadStarted");
+                        payloadStarted.complete(true);
+                    }
+
+                    @Override
+                    public void onPayloadFinished(VirtualMachine vm, int exitCode) {
+                        Log.i(logTag, "onPayloadFinished: " + exitCode);
+                        payloadFinished.complete(true);
+                        forceStop(vm);
+                    }
+                };
+
+        listener.runToFinish(logTag, vm);
+        assertThat(payloadStarted.getNow(false)).isTrue();
+        assertThat(payloadReady.getNow(false)).isTrue();
+        assertThat(payloadFinished.getNow(false)).isTrue();
+        return testResults;
+    }
+
+    @FunctionalInterface
+    protected interface RunTestsAgainstTestService {
+        void runTests(ITestService testService, TestResults testResults) throws Exception;
+    }
 }
diff --git a/tests/hostside/Android.bp b/tests/hostside/Android.bp
index 6e0cf5a..78500af 100644
--- a/tests/hostside/Android.bp
+++ b/tests/hostside/Android.bp
@@ -2,6 +2,54 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
+genrule_defaults {
+    name: "test_avf_debug_policy_overlay",
+    tools: ["dtc"],
+    cmd: "$(location dtc) -I dts -O dtb $(in) -o $(out)",
+}
+
+genrule {
+    name: "test_avf_debug_policy_with_ramdump",
+    defaults: ["test_avf_debug_policy_overlay"],
+    srcs: ["assets/avf_debug_policy_with_ramdump.dts"],
+    out: ["avf_debug_policy_with_ramdump.dtbo"],
+}
+
+genrule {
+    name: "test_avf_debug_policy_without_ramdump",
+    defaults: ["test_avf_debug_policy_overlay"],
+    srcs: ["assets/avf_debug_policy_without_ramdump.dts"],
+    out: ["avf_debug_policy_without_ramdump.dtbo"],
+}
+
+genrule {
+    name: "test_avf_debug_policy_with_console_output",
+    defaults: ["test_avf_debug_policy_overlay"],
+    srcs: ["assets/avf_debug_policy_with_console_output.dts"],
+    out: ["avf_debug_policy_with_console_output.dtbo"],
+}
+
+genrule {
+    name: "test_avf_debug_policy_without_console_output",
+    defaults: ["test_avf_debug_policy_overlay"],
+    srcs: ["assets/avf_debug_policy_without_console_output.dts"],
+    out: ["avf_debug_policy_without_console_output.dtbo"],
+}
+
+genrule {
+    name: "test_avf_debug_policy_with_adb",
+    defaults: ["test_avf_debug_policy_overlay"],
+    srcs: ["assets/avf_debug_policy_with_adb.dts"],
+    out: ["avf_debug_policy_with_adb.dtbo"],
+}
+
+genrule {
+    name: "test_avf_debug_policy_without_adb",
+    defaults: ["test_avf_debug_policy_overlay"],
+    srcs: ["assets/avf_debug_policy_without_adb.dts"],
+    out: ["avf_debug_policy_without_adb.dtbo"],
+}
+
 java_test_host {
     name: "MicrodroidHostTestCases",
     srcs: ["java/**/*.java"],
@@ -10,12 +58,14 @@
         "general-tests",
     ],
     libs: [
+        "androidx.annotation_annotation",
         "tradefed",
     ],
     static_libs: [
         "MicrodroidHostTestHelper",
         "compatibility-host-util",
         "cts-statsd-atom-host-test-utils",
+        "microdroid_payload_metadata",
     ],
     per_testcase_directory: true,
     data: [
@@ -23,6 +73,14 @@
         ":microdroid_general_sepolicy.conf",
         ":test.com.android.virt.pem",
         ":test2.com.android.virt.pem",
+        ":pvmfw_test",
+        ":test_avf_debug_policy_with_ramdump",
+        ":test_avf_debug_policy_without_ramdump",
+        ":test_avf_debug_policy_with_console_output",
+        ":test_avf_debug_policy_without_console_output",
+        ":test_avf_debug_policy_with_adb",
+        ":test_avf_debug_policy_without_adb",
+        "assets/bcc.dat",
     ],
     data_native_bins: [
         "sepolicy-analyze",
@@ -32,7 +90,6 @@
         "initrd_bootconfig",
         "lpmake",
         "lpunpack",
-        "mk_payload",
         "sign_virt_apex",
         "simg2img",
     ],
diff --git a/tests/hostside/assets/avf_debug_policy_with_adb.dts b/tests/hostside/assets/avf_debug_policy_with_adb.dts
new file mode 100644
index 0000000..9ad15dd
--- /dev/null
+++ b/tests/hostside/assets/avf_debug_policy_with_adb.dts
@@ -0,0 +1,18 @@
+/dts-v1/;
+/plugin/;
+
+/ {
+    fragment@avf {
+        target-path = "/";
+
+        __overlay__ {
+            avf {
+                guest {
+                    microdroid {
+                        adb = <1>;
+                    };
+                };
+            };
+        };
+    };
+};
\ No newline at end of file
diff --git a/tests/hostside/assets/avf_debug_policy_with_console_output.dts b/tests/hostside/assets/avf_debug_policy_with_console_output.dts
new file mode 100644
index 0000000..8cf19d6
--- /dev/null
+++ b/tests/hostside/assets/avf_debug_policy_with_console_output.dts
@@ -0,0 +1,18 @@
+/dts-v1/;
+/plugin/;
+
+/ {
+    fragment@avf {
+        target-path = "/";
+
+        __overlay__ {
+            avf {
+                guest {
+                    common {
+                        log = <1>;
+                    };
+                };
+            };
+        };
+    };
+};
\ No newline at end of file
diff --git a/tests/hostside/assets/avf_debug_policy_with_ramdump.dts b/tests/hostside/assets/avf_debug_policy_with_ramdump.dts
new file mode 100644
index 0000000..26db7be
--- /dev/null
+++ b/tests/hostside/assets/avf_debug_policy_with_ramdump.dts
@@ -0,0 +1,21 @@
+/dts-v1/;
+/plugin/;
+
+/ {
+    fragment@avf {
+        target-path = "/";
+
+        __overlay__ {
+            avf {
+                guest {
+                    common {
+                        ramdump = <1>;
+                    };
+                    microdroid {
+                        adb = <1>; // adb is required to check VM's bootargs.
+                    };
+                };
+            };
+        };
+    };
+};
\ No newline at end of file
diff --git a/tests/hostside/assets/avf_debug_policy_without_adb.dts b/tests/hostside/assets/avf_debug_policy_without_adb.dts
new file mode 100644
index 0000000..992e0ff
--- /dev/null
+++ b/tests/hostside/assets/avf_debug_policy_without_adb.dts
@@ -0,0 +1,18 @@
+/dts-v1/;
+/plugin/;
+
+/ {
+    fragment@avf {
+        target-path = "/";
+
+        __overlay__ {
+            avf {
+                guest {
+                    microdroid {
+                        adb = <0>;
+                    };
+                };
+            };
+        };
+    };
+};
\ No newline at end of file
diff --git a/tests/hostside/assets/avf_debug_policy_without_console_output.dts b/tests/hostside/assets/avf_debug_policy_without_console_output.dts
new file mode 100644
index 0000000..da6400c
--- /dev/null
+++ b/tests/hostside/assets/avf_debug_policy_without_console_output.dts
@@ -0,0 +1,18 @@
+/dts-v1/;
+/plugin/;
+
+/ {
+    fragment@avf {
+        target-path = "/";
+
+        __overlay__ {
+            avf {
+                guest {
+                    common {
+                        log = <0>;
+                    };
+                };
+            };
+        };
+    };
+};
\ No newline at end of file
diff --git a/tests/hostside/assets/avf_debug_policy_without_ramdump.dts b/tests/hostside/assets/avf_debug_policy_without_ramdump.dts
new file mode 100644
index 0000000..194e314
--- /dev/null
+++ b/tests/hostside/assets/avf_debug_policy_without_ramdump.dts
@@ -0,0 +1,21 @@
+/dts-v1/;
+/plugin/;
+
+/ {
+    fragment@avf {
+        target-path = "/";
+
+        __overlay__ {
+            avf {
+                guest {
+                    common {
+                        ramdump = <0>;
+                    };
+                    microdroid {
+                        adb = <1>; // adb is required to check VM's bootargs.
+                    };
+                };
+            };
+        };
+    };
+};
\ No newline at end of file
diff --git a/tests/hostside/assets/bcc.dat b/tests/hostside/assets/bcc.dat
new file mode 100644
index 0000000..7ab71f1
--- /dev/null
+++ b/tests/hostside/assets/bcc.dat
Binary files differ
diff --git a/tests/hostside/helper/Android.bp b/tests/hostside/helper/Android.bp
index 6196ec5..e8b6f36 100644
--- a/tests/hostside/helper/Android.bp
+++ b/tests/hostside/helper/Android.bp
@@ -6,6 +6,7 @@
     name: "MicrodroidHostTestHelper",
     srcs: ["java/**/*.java"],
     libs: [
+        "androidx.annotation_annotation",
         "compatibility-tradefed",
         "tradefed",
         "truth-prebuilt",
diff --git a/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java b/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java
index 8d328bc..20a6045 100644
--- a/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java
+++ b/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java
@@ -29,7 +29,6 @@
 import com.android.microdroid.test.common.DeviceProperties;
 import com.android.microdroid.test.common.MetricsProcessor;
 import com.android.tradefed.build.IBuildInfo;
-import com.android.tradefed.config.Option;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.TestDevice;
@@ -42,15 +41,12 @@
 import java.util.Arrays;
 
 public abstract class MicrodroidHostTestCaseBase extends BaseHostJUnit4Test {
-
     protected static final String TEST_ROOT = "/data/local/tmp/virt/";
     protected static final String LOG_PATH = TEST_ROOT + "log.txt";
     protected static final String CONSOLE_PATH = TEST_ROOT + "console.txt";
     private static final int TEST_VM_ADB_PORT = 8000;
     private static final String MICRODROID_SERIAL = "localhost:" + TEST_VM_ADB_PORT;
     private static final String INSTANCE_IMG = "instance.img";
-    private static final String PVMFW_IMG_PATH = TEST_ROOT + "pvmfw.img";
-    private static final String PVMFW_IMG_PATH_PROP = "hypervisor.pvmfw.path";
 
     private static final long MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES = 5;
     protected static final long MICRODROID_COMMAND_TIMEOUT_MILLIS = 30000;
@@ -59,19 +55,6 @@
             (int) (MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES * 60 * 1000
                 / MICRODROID_COMMAND_RETRY_INTERVAL_MILLIS);
 
-    @Option(
-            name = "pvmfw",
-            description =
-                    "Custom pvmfw.img path on host device."
-                            + " If present, it will be pushed to "
-                            + PVMFW_IMG_PATH,
-            mandatory = false)
-    private static String sCustomPvmfwPathOnHost = "";
-
-    private static boolean isEmptyText(String str) {
-        return str == null || str.length() == 0;
-    }
-
     public static void prepareVirtualizationTestSetup(ITestDevice androidDevice)
             throws DeviceNotAvailableException {
         CommandRunner android = new CommandRunner(androidDevice);
@@ -84,13 +67,6 @@
 
         // remove any leftover files under test root
         android.tryRun("rm", "-rf", TEST_ROOT + "*");
-
-        // prepare custom pvmfw.img if necessary
-        if (!isEmptyText(sCustomPvmfwPathOnHost)) {
-            runOnHost("adb", "root");
-            runOnHost("adb", "push", sCustomPvmfwPathOnHost, PVMFW_IMG_PATH);
-            runOnHost("adb", "shell", "setprop", PVMFW_IMG_PATH_PROP, PVMFW_IMG_PATH);
-        }
     }
 
     public static void cleanUpVirtualizationTestSetup(ITestDevice androidDevice)
@@ -104,10 +80,10 @@
         android.tryRun("killall", "crosvm");
         android.tryRun("stop", "virtualizationservice");
         android.tryRun("rm", "-rf", "/data/misc/virtualizationservice/*");
+    }
 
-        if (!isEmptyText(sCustomPvmfwPathOnHost)) {
-            runOnHost("adb", "shell", "setprop", PVMFW_IMG_PATH_PROP, "\"\"");
-        }
+    public boolean isUserBuild() {
+        return DeviceProperties.create(getDevice()::getProperty).isUserBuild();
     }
 
     protected boolean isCuttlefish() {
diff --git a/tests/hostside/helper/java/com/android/microdroid/test/host/Pvmfw.java b/tests/hostside/helper/java/com/android/microdroid/test/host/Pvmfw.java
new file mode 100644
index 0000000..95eaa58
--- /dev/null
+++ b/tests/hostside/helper/java/com/android/microdroid/test/host/Pvmfw.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2023 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.microdroid.test.host;
+
+import static java.nio.ByteOrder.LITTLE_ENDIAN;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Objects;
+import java.nio.ByteBuffer;
+
+/** pvmfw.bin with custom config payloads on host. */
+public final class Pvmfw {
+    private static final int SIZE_8B = 8; // 8 bytes
+    private static final int SIZE_4K = 4 << 10; // 4 KiB, PAGE_SIZE
+    private static final int BUFFER_SIZE = 1024;
+    private static final int HEADER_SIZE = Integer.BYTES * 8; // Header has 8 integers.
+    private static final int HEADER_MAGIC = 0x666d7670;
+    private static final int HEADER_VERSION = getVersion(1, 0);
+    private static final int HEADER_FLAGS = 0;
+
+    @NonNull private final File mPvmfwBinFile;
+    @NonNull private final File mBccFile;
+    @Nullable private final File mDebugPolicyFile;
+
+    private Pvmfw(
+            @NonNull File pvmfwBinFile, @NonNull File bccFile, @Nullable File debugPolicyFile) {
+        mPvmfwBinFile = Objects.requireNonNull(pvmfwBinFile);
+        mBccFile = Objects.requireNonNull(bccFile);
+        mDebugPolicyFile = debugPolicyFile;
+    }
+
+    /**
+     * Serializes pvmfw.bin and its config, as written in the <a
+     * href="https://android.googlesource.com/platform/packages/modules/Virtualization/+/master/pvmfw/README.md">README.md</a>
+     */
+    public void serialize(@NonNull File outFile) throws IOException {
+        Objects.requireNonNull(outFile);
+
+        int bccOffset = HEADER_SIZE;
+        int bccSize = (int) mBccFile.length();
+
+        int debugPolicyOffset = alignTo(bccOffset + bccSize, SIZE_8B);
+        int debugPolicySize = mDebugPolicyFile == null ? 0 : (int) mDebugPolicyFile.length();
+
+        int totalSize = debugPolicyOffset + debugPolicySize;
+
+        ByteBuffer header = ByteBuffer.allocate(HEADER_SIZE).order(LITTLE_ENDIAN);
+        header.putInt(HEADER_MAGIC);
+        header.putInt(HEADER_VERSION);
+        header.putInt(totalSize);
+        header.putInt(HEADER_FLAGS);
+        header.putInt(bccOffset);
+        header.putInt(bccSize);
+        header.putInt(debugPolicyOffset);
+        header.putInt(debugPolicySize);
+
+        try (FileOutputStream pvmfw = new FileOutputStream(outFile)) {
+            appendFile(pvmfw, mPvmfwBinFile);
+            padTo(pvmfw, SIZE_4K);
+            pvmfw.write(header.array());
+            padTo(pvmfw, HEADER_SIZE);
+            appendFile(pvmfw, mBccFile);
+            if (mDebugPolicyFile != null) {
+                padTo(pvmfw, SIZE_8B);
+                appendFile(pvmfw, mDebugPolicyFile);
+            }
+            padTo(pvmfw, SIZE_4K);
+        }
+    }
+
+    private void appendFile(@NonNull FileOutputStream out, @NonNull File inFile)
+            throws IOException {
+        byte buffer[] = new byte[BUFFER_SIZE];
+        try (FileInputStream in = new FileInputStream(inFile)) {
+            int size;
+            while (true) {
+                size = in.read(buffer);
+                if (size < 0) {
+                    return;
+                }
+                out.write(buffer, /* offset= */ 0, size);
+            }
+        }
+    }
+
+    private void padTo(@NonNull FileOutputStream out, int size) throws IOException {
+        int streamSize = (int) out.getChannel().size();
+        for (int i = streamSize; i < alignTo(streamSize, size); i++) {
+            out.write(0); // write byte.
+        }
+    }
+
+    private static int alignTo(int x, int size) {
+        return (x + size - 1) & ~(size - 1);
+    }
+
+    private static int getVersion(int major, int minor) {
+        return ((major & 0xFFFF) << 16) | (minor & 0xFFFF);
+    }
+
+    /** Builder for {@link Pvmfw}. */
+    public static final class Builder {
+        @NonNull private final File mPvmfwBinFile;
+        @NonNull private final File mBccFile;
+        @Nullable private File mDebugPolicyFile;
+
+        public Builder(@NonNull File pvmfwBinFile, @NonNull File bccFile) {
+            mPvmfwBinFile = Objects.requireNonNull(pvmfwBinFile);
+            mBccFile = Objects.requireNonNull(bccFile);
+        }
+
+        @NonNull
+        public Builder setDebugPolicyOverlay(@Nullable File debugPolicyFile) {
+            mDebugPolicyFile = debugPolicyFile;
+            return this;
+        }
+
+        @NonNull
+        public Pvmfw build() {
+            return new Pvmfw(mPvmfwBinFile, mBccFile, mDebugPolicyFile);
+        }
+    }
+}
diff --git a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
index 0623ff2..687756e 100644
--- a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
+++ b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
@@ -50,6 +50,7 @@
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.RunUtil;
 import com.android.tradefed.util.xml.AbstractXmlParser;
+import com.android.virt.PayloadMetadata;
 
 import org.json.JSONArray;
 import org.json.JSONObject;
@@ -72,6 +73,7 @@
 import java.io.PipedOutputStream;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -91,9 +93,6 @@
     private static final int MIN_MEM_ARM64 = 145;
     private static final int MIN_MEM_X86_64 = 196;
 
-    // Number of vCPUs for testing purpose
-    private static final int NUM_VCPUS = 3;
-
     private static final int BOOT_COMPLETE_TIMEOUT = 30000; // 30 seconds
 
     private static final Pattern sCIDPattern = Pattern.compile("with CID (\\d+)");
@@ -134,42 +133,14 @@
 
     private void createPayloadMetadata(List<ActiveApexInfo> apexes, File payloadMetadata)
             throws Exception {
-        // mk_payload's config
-        File configFile = new File(payloadMetadata.getParentFile(), "payload_config.json");
-        JSONObject config = new JSONObject();
-        config.put(
-                "apk",
-                new JSONObject(Map.of("name", "microdroid-apk", "path", "", "idsig_path", "")));
-        config.put("payload_config_path", "/mnt/apk/assets/vm_config.json");
-        config.put(
-                "apexes",
-                new JSONArray(
+        PayloadMetadata.write(
+                PayloadMetadata.metadata(
+                        "/mnt/apk/assets/vm_config.json",
+                        PayloadMetadata.apk("microdroid-apk"),
                         apexes.stream()
-                                .map(apex -> new JSONObject(Map.of("name", apex.name, "path", "")))
-                                .collect(toList())));
-        FileUtil.writeToFile(config.toString(), configFile);
-
-        RunUtil runUtil = new RunUtil();
-        String command =
-                String.join(
-                        " ",
-                        findTestFile("mk_payload").getAbsolutePath(),
-                        "--metadata-only",
-                        configFile.getAbsolutePath(),
-                        payloadMetadata.getAbsolutePath());
-        // mk_payload should run fast enough
-        CommandResult result = runUtil.runTimedCmd(5000, "/bin/bash", "-c", command);
-        String out = result.getStdout();
-        String err = result.getStderr();
-        assertWithMessage(
-                        "creating payload metadata failed:\n\tout: "
-                                + out
-                                + "\n\terr: "
-                                + err
-                                + "\n")
-                .about(command_results())
-                .that(result)
-                .isSuccess();
+                                .map(apex -> PayloadMetadata.apex(apex.name))
+                                .collect(toList())),
+                payloadMetadata);
     }
 
     private void resignVirtApex(
@@ -221,6 +192,14 @@
         assertThat(callable.call(), matcher);
     }
 
+    private int getDeviceNumCpus(CommandRunner runner) throws DeviceNotAvailableException {
+        return Integer.parseInt(runner.run("nproc --all").trim());
+    }
+
+    private int getDeviceNumCpus(ITestDevice device) throws DeviceNotAvailableException {
+        return getDeviceNumCpus(new CommandRunner(device));
+    }
+
     static class ActiveApexInfo {
         public String name;
         public String path;
@@ -442,7 +421,7 @@
                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
                         .debugLevel("full")
                         .memoryMib(minMemorySize())
-                        .numCpus(NUM_VCPUS)
+                        .cpuTopology("match_host")
                         .protectedVm(protectedVm)
                         .build(getAndroidDevice());
 
@@ -513,24 +492,7 @@
         vmInfo.mProcess.destroy();
     }
 
-    private boolean isTombstoneGenerated(String configPath, String... crashCommand)
-            throws Exception {
-        // Note this test relies on logcat values being printed by tombstone_transmit on
-        // and the reeceiver on host (virtualization_service)
-        mMicrodroidDevice =
-                MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
-                        .debugLevel("full")
-                        .memoryMib(minMemorySize())
-                        .numCpus(NUM_VCPUS)
-                        .build(getAndroidDevice());
-        mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
-        mMicrodroidDevice.enableAdbRoot();
-
-        CommandRunner microdroid = new CommandRunner(mMicrodroidDevice);
-        microdroid.run(crashCommand);
-
-        // check until microdroid is shut down
-        CommandRunner android = new CommandRunner(getDevice());
+    private void waitForCrosvmExit(CommandRunner android) throws Exception {
         // TODO: improve crosvm exit check. b/258848245
         android.runWithTimeout(
                 15000,
@@ -539,8 +501,12 @@
                 "1",
                 "-e",
                 "'virtualizationmanager::crosvm.*exited with status exit status:'");
+    }
 
-        // Check that tombstone is received (from host logcat)
+    private boolean isTombstoneReceivedFromHostLogcat() throws Exception {
+        // Note this method relies on logcat values being printed by the receiver on host
+        // userspace crash log: virtualizationservice/src/aidl.rs
+        // kernel ramdump log: virtualizationmanager/src/crosvm.rs
         String ramdumpRegex =
                 "Received [0-9]+ bytes from guest & wrote to tombstone file|"
                         + "Ramdump \"[^ ]+/ramdump\" sent to tombstoned";
@@ -560,11 +526,34 @@
         return !result.trim().isEmpty();
     }
 
+    private boolean isTombstoneGeneratedWithCmd(
+            boolean protectedVm, String configPath, String... crashCommand) throws Exception {
+        mMicrodroidDevice =
+                MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
+                        .debugLevel("full")
+                        .memoryMib(minMemorySize())
+                        .cpuTopology("match_host")
+                        .protectedVm(protectedVm)
+                        .build(getAndroidDevice());
+        mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
+        mMicrodroidDevice.enableAdbRoot();
+
+        CommandRunner microdroid = new CommandRunner(mMicrodroidDevice);
+        microdroid.run(crashCommand);
+
+        // check until microdroid is shut down
+        CommandRunner android = new CommandRunner(getDevice());
+        waitForCrosvmExit(android);
+
+        return isTombstoneReceivedFromHostLogcat();
+    }
+
     @Test
     public void testTombstonesAreGeneratedUponUserspaceCrash() throws Exception {
         assertThat(
-                        isTombstoneGenerated(
-                                "assets/vm_config_crash.json",
+                        isTombstoneGeneratedWithCmd(
+                                false,
+                                "assets/vm_config.json",
                                 "kill",
                                 "-SIGSEGV",
                                 "$(pidof microdroid_launcher)"))
@@ -574,20 +563,22 @@
     @Test
     public void testTombstonesAreNotGeneratedIfNotExportedUponUserspaceCrash() throws Exception {
         assertThat(
-                        isTombstoneGenerated(
-                                "assets/vm_config_crash_no_tombstone.json",
+                        isTombstoneGeneratedWithCmd(
+                                false,
+                                "assets/vm_config_no_tombstone.json",
                                 "kill",
                                 "-SIGSEGV",
                                 "$(pidof microdroid_launcher)"))
                 .isFalse();
     }
 
-    @Test
-    public void testTombstonesAreGeneratedUponKernelCrash() throws Exception {
+    private void testTombstonesAreGeneratedUponKernelCrash(boolean protectedVm) throws Exception {
         assumeFalse("Cuttlefish is not supported", isCuttlefish());
+        assumeFalse("Skipping test because ramdump is disabled on user build", isUserBuild());
         assertThat(
-                        isTombstoneGenerated(
-                                "assets/vm_config_crash.json",
+                        isTombstoneGeneratedWithCmd(
+                                protectedVm,
+                                "assets/vm_config.json",
                                 "echo",
                                 "c",
                                 ">",
@@ -596,6 +587,76 @@
     }
 
     @Test
+    public void testTombstonesAreGeneratedUponKernelCrashOnNonPvm() throws Exception {
+        testTombstonesAreGeneratedUponKernelCrash(false);
+    }
+
+    @Test
+    public void testTombstonesAreGeneratedUponKernelCrashOnPvm() throws Exception {
+        assumeTrue(
+                "Protected VMs are not supported",
+                getAndroidDevice().supportsMicrodroid(/*protectedVm=*/ true));
+        testTombstonesAreGeneratedUponKernelCrash(true);
+    }
+
+    private boolean isTombstoneGeneratedWithVmRunApp(boolean debuggable, String... additionalArgs)
+            throws Exception {
+        // we can't use microdroid builder as it wants ADB connection (debuggable)
+        CommandRunner android = new CommandRunner(getDevice());
+
+        android.run("rm", "-rf", TEST_ROOT + "*");
+        android.run("mkdir", "-p", TEST_ROOT + "*");
+
+        final String apkPath = getPathForPackage(PACKAGE_NAME);
+        final String idsigPath = TEST_ROOT + "idsig";
+        final String instanceImgPath = TEST_ROOT + "instance.img";
+        List<String> cmd =
+                new ArrayList<>(
+                        Arrays.asList(
+                                VIRT_APEX + "bin/vm",
+                                "run-app",
+                                "--debug",
+                                debuggable ? "full" : "none",
+                                apkPath,
+                                idsigPath,
+                                instanceImgPath));
+        Collections.addAll(cmd, additionalArgs);
+
+        android.run(cmd.toArray(new String[0]));
+        return isTombstoneReceivedFromHostLogcat();
+    }
+
+    private boolean isTombstoneGeneratedWithCrashPayload(boolean debuggable) throws Exception {
+        return isTombstoneGeneratedWithVmRunApp(
+                debuggable, "--payload-binary-name", "MicrodroidCrashNativeLib.so");
+    }
+
+    @Test
+    public void testTombstonesAreGeneratedWithCrashPayload() throws Exception {
+        assertThat(isTombstoneGeneratedWithCrashPayload(true /* debuggable */)).isTrue();
+    }
+
+    @Test
+    public void testTombstonesAreNotGeneratedWithCrashPayloadWhenNonDebuggable() throws Exception {
+        assertThat(isTombstoneGeneratedWithCrashPayload(false /* debuggable */)).isFalse();
+    }
+
+    private boolean isTombstoneGeneratedWithCrashConfig(boolean debuggable) throws Exception {
+        return isTombstoneGeneratedWithVmRunApp(
+                debuggable, "--config-path", "assets/vm_config_crash.json");
+    }
+
+    @Test
+    public void testTombstonesAreGeneratedWithCrashConfig() throws Exception {
+        assertThat(isTombstoneGeneratedWithCrashConfig(true /* debuggable */)).isTrue();
+    }
+
+    @Test
+    public void testTombstonesAreNotGeneratedWithCrashConfigWhenNonDebuggable() throws Exception {
+        assertThat(isTombstoneGeneratedWithCrashConfig(false /* debuggable */)).isFalse();
+    }
+
+    @Test
     public void testTelemetryPushedAtoms() throws Exception {
         // Reset statsd config and report before the test
         ConfigUtils.removeConfig(getDevice());
@@ -616,7 +677,7 @@
                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
                         .debugLevel("full")
                         .memoryMib(minMemorySize())
-                        .numCpus(NUM_VCPUS)
+                        .cpuTopology("match_host")
                         .build(device);
         microdroid.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
         device.shutdownMicrodroid(microdroid);
@@ -644,7 +705,7 @@
         assertThat(atomVmCreationRequested.getVmIdentifier()).isEqualTo("VmRunApp");
         assertThat(atomVmCreationRequested.getConfigType())
                 .isEqualTo(AtomsProto.VmCreationRequested.ConfigType.VIRTUAL_MACHINE_APP_CONFIG);
-        assertThat(atomVmCreationRequested.getNumCpus()).isEqualTo(NUM_VCPUS);
+        assertThat(atomVmCreationRequested.getNumCpus()).isEqualTo(getDeviceNumCpus(device));
         assertThat(atomVmCreationRequested.getMemoryMib()).isEqualTo(minMemorySize());
         assertThat(atomVmCreationRequested.getApexes())
                 .isEqualTo("com.android.art:com.android.compos:com.android.sdkext");
@@ -679,7 +740,7 @@
                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
                         .debugLevel("full")
                         .memoryMib(minMemorySize())
-                        .numCpus(NUM_VCPUS)
+                        .cpuTopology("match_host")
                         .build(getAndroidDevice());
         mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
         CommandRunner microdroid = new CommandRunner(mMicrodroidDevice);
@@ -708,8 +769,7 @@
         assertThat(android.tryRun("egrep", "'avc:[[:space:]]{1,2}denied'", LOG_PATH)).isNull();
         assertThat(android.tryRun("egrep", "'avc:[[:space:]]{1,2}denied'", CONSOLE_PATH)).isNull();
 
-        assertThat(microdroid.run("cat /proc/cpuinfo | grep processor | wc -l"))
-                .isEqualTo(Integer.toString(NUM_VCPUS));
+        assertThat(getDeviceNumCpus(microdroid)).isEqualTo(getDeviceNumCpus(android));
 
         // Check that selinux is enabled
         assertThat(microdroid.run("getenforce")).isEqualTo("Enforcing");
@@ -745,7 +805,7 @@
                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
                         .debugLevel("full")
                         .memoryMib(minMemorySize())
-                        .numCpus(NUM_VCPUS)
+                        .cpuTopology("match_host")
                         .build(getAndroidDevice());
         mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
         mMicrodroidDevice.enableAdbRoot();
@@ -785,6 +845,7 @@
         assumeTrue(
                 "Protected VMs are not supported",
                 getAndroidDevice().supportsMicrodroid(/*protectedVm=*/ true));
+        assumeTrue("Test requires adb unroot", getDevice().disableAdbRoot());
         CommandRunner android = new CommandRunner(getDevice());
 
         // Pull etc/microdroid.json
diff --git a/tests/hostside/java/com/android/microdroid/test/PvmfwDebugPolicyHostTests.java b/tests/hostside/java/com/android/microdroid/test/PvmfwDebugPolicyHostTests.java
new file mode 100644
index 0000000..10f7003
--- /dev/null
+++ b/tests/hostside/java/com/android/microdroid/test/PvmfwDebugPolicyHostTests.java
@@ -0,0 +1,300 @@
+/*
+ * Copyright 2023 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.microdroid.test;
+
+import static com.android.tradefed.device.TestDevice.MicrodroidBuilder;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assume.assumeTrue;
+import static org.junit.Assume.assumeFalse;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.microdroid.test.host.CommandRunner;
+import com.android.microdroid.test.host.MicrodroidHostTestCaseBase;
+import com.android.microdroid.test.host.Pvmfw;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.TestDevice;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.FileUtil;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+import java.io.FileNotFoundException;
+
+/** Tests debug policy of pvmfw.bin with custom debug policy */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class PvmfwDebugPolicyHostTests extends MicrodroidHostTestCaseBase {
+    @NonNull private static final String PVMFW_FILE_NAME = "pvmfw_test.bin";
+    @NonNull private static final String BCC_FILE_NAME = "bcc.dat";
+    @NonNull private static final String PACKAGE_FILE_NAME = "MicrodroidTestApp.apk";
+    @NonNull private static final String PACKAGE_NAME = "com.android.microdroid.test";
+    @NonNull private static final String MICRODROID_DEBUG_FULL = "full";
+    @NonNull private static final String MICRODROID_DEBUG_NONE = "none";
+    @NonNull private static final String MICRODROID_CONFIG_PATH = "assets/vm_config_apex.json";
+    @NonNull private static final String MICRODROID_LOG_PATH = TEST_ROOT + "log.txt";
+    private static final int BOOT_COMPLETE_TIMEOUT_MS = 30000; // 30 seconds
+    private static final int BOOT_FAILURE_WAIT_TIME_MS = 10000; // 10 seconds
+    private static final int CONSOLE_OUTPUT_WAIT_MS = 5000; // 5 seconds
+
+    @NonNull private static final String CUSTOM_PVMFW_FILE_PREFIX = "pvmfw";
+    @NonNull private static final String CUSTOM_PVMFW_FILE_SUFFIX = ".bin";
+    @NonNull private static final String CUSTOM_PVMFW_IMG_PATH = TEST_ROOT + PVMFW_FILE_NAME;
+    @NonNull private static final String CUSTOM_PVMFW_IMG_PATH_PROP = "hypervisor.pvmfw.path";
+
+    @NonNull private static final String MICRODROID_CMDLINE_PATH = "/proc/cmdline";
+    @NonNull private static final String MICRODROID_DT_ROOT_PATH = "/proc/device-tree";
+
+    @NonNull
+    private static final String MICRODROID_DT_BOOTARGS_PATH =
+            MICRODROID_DT_ROOT_PATH + "/chosen/bootargs";
+
+    @NonNull
+    private static final String MICRODROID_DT_RAMDUMP_PATH =
+            MICRODROID_DT_ROOT_PATH + "/avf/guest/common/ramdump";
+
+    @NonNull private static final String HEX_STRING_ZERO = "00000000";
+    @NonNull private static final String HEX_STRING_ONE = "00000001";
+
+    @Nullable private static File mPvmfwBinFileOnHost;
+    @Nullable private static File mBccFileOnHost;
+
+    @Nullable private TestDevice mAndroidDevice;
+    @Nullable private ITestDevice mMicrodroidDevice;
+    @Nullable private File mCustomPvmfwBinFileOnHost;
+
+    @Before
+    public void setUp() throws Exception {
+        mAndroidDevice = (TestDevice) Objects.requireNonNull(getDevice());
+        assumeTrue(
+                "Skip if protected VMs are not supported",
+                mAndroidDevice.supportsMicrodroid(/* protectedVm= */ true));
+        assumeFalse("Test requires setprop for using custom pvmfw and adb root", isUserBuild());
+
+        mAndroidDevice.enableAdbRoot();
+
+        // tradefed copies the test artfacts under /tmp when running tests,
+        // so we should *find* the artifacts with the file name.
+        mPvmfwBinFileOnHost =
+                getTestInformation().getDependencyFile(PVMFW_FILE_NAME, /* targetFirst= */ false);
+        mBccFileOnHost =
+                getTestInformation().getDependencyFile(BCC_FILE_NAME, /* targetFirst= */ false);
+
+        // Check device capability
+        testIfDeviceIsCapable(mAndroidDevice);
+        assumeTrue(
+                "Protected VMs are not supported",
+                mAndroidDevice.supportsMicrodroid(/*protectedVm=*/ true));
+
+        // Prepare for loading pvmfw.bin
+        // File will be setup in individual test,
+        // and then pushed to device in launchProtectedVmAndWaitForBootCompleted.
+        mCustomPvmfwBinFileOnHost =
+                FileUtil.createTempFile(CUSTOM_PVMFW_FILE_PREFIX, CUSTOM_PVMFW_FILE_SUFFIX);
+        mAndroidDevice.setProperty(CUSTOM_PVMFW_IMG_PATH_PROP, CUSTOM_PVMFW_IMG_PATH);
+
+        // Prepare for launching microdroid
+        mAndroidDevice.installPackage(findTestFile(PACKAGE_FILE_NAME), /* reinstall */ false);
+        prepareVirtualizationTestSetup(mAndroidDevice);
+        mMicrodroidDevice = null;
+    }
+
+    @After
+    public void shutdown() throws Exception {
+        if (!mAndroidDevice.supportsMicrodroid(/* protectedVm= */ true)) {
+            return;
+        }
+        if (mMicrodroidDevice != null) {
+            mAndroidDevice.shutdownMicrodroid(mMicrodroidDevice);
+            mMicrodroidDevice = null;
+        }
+        mAndroidDevice.uninstallPackage(PACKAGE_NAME);
+
+        // Cleanup for custom pvmfw.bin
+        mAndroidDevice.setProperty(CUSTOM_PVMFW_IMG_PATH_PROP, "");
+        FileUtil.deleteFile(mCustomPvmfwBinFileOnHost);
+
+        cleanUpVirtualizationTestSetup(mAndroidDevice);
+
+        mAndroidDevice.disableAdbRoot();
+    }
+
+    @Test
+    public void testRamdump() throws Exception {
+        Pvmfw pvmfw = createPvmfw("avf_debug_policy_with_ramdump.dtbo");
+        pvmfw.serialize(mCustomPvmfwBinFileOnHost);
+        mMicrodroidDevice = launchProtectedVmAndWaitForBootCompleted(MICRODROID_DEBUG_FULL);
+
+        assertThat(readMicrodroidFileAsString(MICRODROID_CMDLINE_PATH)).contains("crashkernel=");
+        assertThat(readMicrodroidFileAsString(MICRODROID_DT_BOOTARGS_PATH))
+                .contains("crashkernel=");
+        assertThat(readMicrodroidFileAsHexString(MICRODROID_DT_RAMDUMP_PATH))
+                .isEqualTo(HEX_STRING_ONE);
+    }
+
+    @Test
+    public void testNoRamdump() throws Exception {
+        Pvmfw pvmfw = createPvmfw("avf_debug_policy_without_ramdump.dtbo");
+        pvmfw.serialize(mCustomPvmfwBinFileOnHost);
+        mMicrodroidDevice = launchProtectedVmAndWaitForBootCompleted(MICRODROID_DEBUG_FULL);
+
+        assertThat(readMicrodroidFileAsString(MICRODROID_CMDLINE_PATH))
+                .doesNotContain("crashkernel=");
+        assertThat(readMicrodroidFileAsString(MICRODROID_DT_BOOTARGS_PATH))
+                .doesNotContain("crashkernel=");
+        assertThat(readMicrodroidFileAsHexString(MICRODROID_DT_RAMDUMP_PATH))
+                .isEqualTo(HEX_STRING_ZERO);
+    }
+
+    @Test
+    public void testConsoleOutput() throws Exception {
+        Pvmfw pvmfw = createPvmfw("avf_debug_policy_with_console_output.dtbo");
+        pvmfw.serialize(mCustomPvmfwBinFileOnHost);
+
+        CommandResult result = tryLaunchProtectedNonDebuggableVm();
+
+        assertWithMessage("Microdroid's console message should have been enabled")
+                .that(hasConsoleOutput(result))
+                .isTrue();
+    }
+
+    @Test
+    public void testNoConsoleOutput() throws Exception {
+        Pvmfw pvmfw = createPvmfw("avf_debug_policy_without_console_output.dtbo");
+        pvmfw.serialize(mCustomPvmfwBinFileOnHost);
+
+        CommandResult result = tryLaunchProtectedNonDebuggableVm();
+
+        assertWithMessage("Microdroid's console message shouldn't have been disabled")
+                .that(hasConsoleOutput(result))
+                .isFalse();
+    }
+
+    @Test
+    public void testNoAdb_boots() throws Exception {
+        Pvmfw pvmfw = createPvmfw("avf_debug_policy_without_adb.dtbo");
+        pvmfw.serialize(mCustomPvmfwBinFileOnHost);
+
+        // VM would boot, but cannot verify directly because of no adbd in the VM.
+        CommandResult result = tryLaunchProtectedNonDebuggableVm();
+        assertThat(result.getStatus()).isEqualTo(CommandStatus.TIMED_OUT);
+        assertWithMessage("Microdroid should have booted")
+                .that(result.getStderr())
+                .contains("payload is ready");
+    }
+
+    @Test
+    public void testNoAdb_noConnection() throws Exception {
+        Pvmfw pvmfw = createPvmfw("avf_debug_policy_without_adb.dtbo");
+        pvmfw.serialize(mCustomPvmfwBinFileOnHost);
+
+        try {
+            launchProtectedVmAndWaitForBootCompleted(
+                    MICRODROID_DEBUG_NONE, BOOT_FAILURE_WAIT_TIME_MS);
+            assertWithMessage("adb shouldn't be available").fail();
+        } catch (Exception e) {
+            // expected exception. passthrough.
+        }
+    }
+
+    @NonNull
+    private String readMicrodroidFileAsString(@NonNull String path)
+            throws DeviceNotAvailableException {
+        return new CommandRunner(mMicrodroidDevice).run("cat", path);
+    }
+
+    @NonNull
+    private String readMicrodroidFileAsHexString(@NonNull String path)
+            throws DeviceNotAvailableException {
+        return new CommandRunner(mMicrodroidDevice).run("xxd", "-p", path);
+    }
+
+    @NonNull
+    private Pvmfw createPvmfw(@NonNull String debugPolicyFileName) throws FileNotFoundException {
+        File file =
+                getTestInformation()
+                        .getDependencyFile(debugPolicyFileName, /* targetFirst= */ false);
+        return new Pvmfw.Builder(mPvmfwBinFileOnHost, mBccFileOnHost)
+                .setDebugPolicyOverlay(file)
+                .build();
+    }
+
+    @NonNull
+    private boolean hasConsoleOutput(CommandResult result) throws DeviceNotAvailableException {
+        return result.getStdout().contains("Run /init as init process");
+    }
+
+    private ITestDevice launchProtectedVmAndWaitForBootCompleted(String debugLevel)
+            throws DeviceNotAvailableException {
+        return launchProtectedVmAndWaitForBootCompleted(debugLevel, BOOT_COMPLETE_TIMEOUT_MS);
+    }
+
+    private ITestDevice launchProtectedVmAndWaitForBootCompleted(
+            String debugLevel, long adbTimeoutMs) throws DeviceNotAvailableException {
+        mMicrodroidDevice =
+                MicrodroidBuilder.fromDevicePath(
+                                getPathForPackage(PACKAGE_NAME), MICRODROID_CONFIG_PATH)
+                        .debugLevel(debugLevel)
+                        .protectedVm(/* protectedVm= */ true)
+                        .addBootFile(mCustomPvmfwBinFileOnHost, PVMFW_FILE_NAME)
+                        .setAdbConnectTimeoutMs(adbTimeoutMs)
+                        .build(mAndroidDevice);
+        assertThat(mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT_MS)).isTrue();
+        assertThat(mMicrodroidDevice.enableAdbRoot()).isTrue();
+        return mMicrodroidDevice;
+    }
+
+    // Try to launch protected non-debuggable VM for a while and quit.
+    // Non-debuggable VM doesn't enable adb, so there's no ITestDevice instance of it.
+    private CommandResult tryLaunchProtectedNonDebuggableVm() throws DeviceNotAvailableException {
+        // Can't use MicrodroidBuilder because it expects adb connection
+        // but non-debuggable VM doesn't enable adb.
+        CommandRunner runner = new CommandRunner(mAndroidDevice);
+        runner.run("mkdir", "-p", TEST_ROOT);
+        mAndroidDevice.pushFile(mCustomPvmfwBinFileOnHost, TEST_ROOT + PVMFW_FILE_NAME);
+
+        // This will fail because app wouldn't finish itself.
+        // But let's run the app once and get logs.
+        String command =
+                String.join(
+                        " ",
+                        "/apex/com.android.virt/bin/vm",
+                        "run-app",
+                        "--log",
+                        MICRODROID_LOG_PATH,
+                        "--protected",
+                        getPathForPackage(PACKAGE_NAME),
+                        TEST_ROOT + "idsig",
+                        TEST_ROOT + "instance.img",
+                        "--config-path",
+                        MICRODROID_CONFIG_PATH);
+        return mAndroidDevice.executeShellV2Command(
+                command, CONSOLE_OUTPUT_WAIT_MS, TimeUnit.MILLISECONDS, /* retryAttempts= */ 0);
+    }
+}
diff --git a/tests/hostside/tools/Android.bp b/tests/hostside/tools/Android.bp
new file mode 100644
index 0000000..f3cc275
--- /dev/null
+++ b/tests/hostside/tools/Android.bp
@@ -0,0 +1,10 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_binary_host {
+    name: "pvmfw-tool",
+    manifest: "pvmfw-tool-manifest.txt",
+    srcs: ["PvmfwTool.java"],
+    static_libs: ["MicrodroidHostTestHelper"],
+}
diff --git a/tests/hostside/tools/PvmfwTool.java b/tests/hostside/tools/PvmfwTool.java
new file mode 100644
index 0000000..18dd6d7
--- /dev/null
+++ b/tests/hostside/tools/PvmfwTool.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2023 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.microdroid;
+
+import com.android.microdroid.test.host.Pvmfw;
+
+import java.io.File;
+import java.io.IOException;
+
+/** CLI for {@link com.android.microdroid.test.host.Pvmfw}. */
+public class PvmfwTool {
+    public static void printUsage() {
+        System.out.println("pvmfw-tool: Appends pvmfw.bin and config payloads.");
+        System.out.println("Requires BCC and debug policy dtbo files");
+        System.out.println("");
+        System.out.println("Usage: pvmfw-tool <pvmfw_with_config> <pvmfw_bin> <bcc.dat> <dp.dtbo>");
+    }
+
+    public static void main(String[] args) {
+        if (args.length != 4) {
+            printUsage();
+            System.exit(1);
+        }
+
+        File out = new File(args[0]);
+        File pvmfw_bin = new File(args[1]);
+        File bcc_dat = new File(args[2]);
+        File dtbo = new File(args[3]);
+
+        try {
+            Pvmfw pvmfw = new Pvmfw.Builder(pvmfw_bin, bcc_dat).setDebugPolicyOverlay(dtbo).build();
+            pvmfw.serialize(out);
+        } catch (IOException e) {
+            e.printStackTrace();
+            printUsage();
+            System.exit(1);
+        }
+    }
+}
diff --git a/tests/hostside/tools/pvmfw-tool-manifest.txt b/tests/hostside/tools/pvmfw-tool-manifest.txt
new file mode 100644
index 0000000..dc71fd2
--- /dev/null
+++ b/tests/hostside/tools/pvmfw-tool-manifest.txt
@@ -0,0 +1,2 @@
+Manifest-Version: 1.0
+Main-Class: com.android.microdroid.PvmfwTool
diff --git a/tests/testapk/Android.bp b/tests/testapk/Android.bp
index 5f9b915..fe8f5c9 100644
--- a/tests/testapk/Android.bp
+++ b/tests/testapk/Android.bp
@@ -2,12 +2,26 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-android_test {
-    name: "MicrodroidTestApp",
+java_defaults {
+    name: "MicrodroidTestAppsDefaults",
     test_suites: [
         "cts",
         "general-tests",
     ],
+    static_libs: [
+        "com.android.microdroid.testservice-java",
+        "com.android.microdroid.test.vmshare_service-java",
+    ],
+    sdk_version: "test_current",
+    jni_uses_platform_apis: true,
+    use_embedded_native_libs: true,
+    // We only support 64-bit ABI, but CTS demands all APKs to be multi-ABI.
+    compile_multilib: "both",
+}
+
+android_test {
+    name: "MicrodroidTestApp",
+    defaults: ["MicrodroidTestAppsDefaults"],
     srcs: ["src/java/**/*.java"],
     static_libs: [
         "MicrodroidDeviceTestHelper",
@@ -15,27 +29,28 @@
         "androidx.test.ext.junit",
         "authfs_test_apk_assets",
         "cbor-java",
-        "com.android.microdroid.testservice-java",
         "truth-prebuilt",
         "compatibility-common-util-devicesidelib",
+        "measure_io_as_jar",
     ],
-    sdk_version: "test_current",
     jni_libs: [
         "MicrodroidTestNativeLib",
         "MicrodroidIdleNativeLib",
         "MicrodroidEmptyNativeLib",
         "MicrodroidExitNativeLib",
         "MicrodroidPrivateLinkingNativeLib",
+        "MicrodroidCrashNativeLib",
     ],
-    jni_uses_platform_apis: true,
-    use_embedded_native_libs: true,
-    // We only support 64-bit ABI, but CTS demands all APKs to be multi-ABI.
-    compile_multilib: "both",
     min_sdk_version: "33",
+    // Defined in ../vmshareapp/Android.bp
+    data: [":MicrodroidVmShareApp"],
 }
 
-cc_library_shared {
-    name: "MicrodroidTestNativeLib",
+// Defaults shared between MicrodroidTestNativeLib and MicrodroidPayloadInOtherAppNativeLib shared
+// libs. They are expected to share everything apart from the name, so that one app
+// (MicrodroidTestApp) can start a payload defined in the another app (MicrodroidVmShareApp).
+cc_defaults {
+    name: "MicrodroidTestNativeLibDefaults",
     srcs: ["src/native/testbinary.cpp"],
     stl: "libc++_static",
     header_libs: ["vm_payload_restricted_headers"],
@@ -48,6 +63,7 @@
     static_libs: [
         "com.android.microdroid.testservice-ndk",
         "libbase",
+        "libfstab",
         "libfsverity_digests_proto_cc",
         "liblog",
         "libprotobuf-cpp-lite-ndk",
@@ -55,6 +71,16 @@
 }
 
 cc_library_shared {
+    name: "MicrodroidPayloadInOtherAppNativeLib",
+    defaults: ["MicrodroidTestNativeLibDefaults"],
+}
+
+cc_library_shared {
+    name: "MicrodroidTestNativeLib",
+    defaults: ["MicrodroidTestNativeLibDefaults"],
+}
+
+cc_library_shared {
     name: "MicrodroidTestNativeLibSub",
     srcs: ["src/native/testlib.cpp"],
     stl: "libc++_static",
@@ -92,3 +118,11 @@
     shared_libs: ["libselinux#latest"],
     stl: "libc++_static",
 }
+
+// A payload that crashes immediately on start
+cc_library_shared {
+    name: "MicrodroidCrashNativeLib",
+    srcs: ["src/native/crashbinary.cpp"],
+    header_libs: ["vm_payload_headers"],
+    stl: "libc++_static",
+}
diff --git a/tests/testapk/AndroidManifest.xml b/tests/testapk/AndroidManifest.xml
index fefd20a..2ea3f6c 100644
--- a/tests/testapk/AndroidManifest.xml
+++ b/tests/testapk/AndroidManifest.xml
@@ -19,6 +19,9 @@
     <uses-permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE" />
     <uses-sdk android:minSdkVersion="33" android:targetSdkVersion="33" />
     <uses-feature android:name="android.software.virtualization_framework" android:required="false" />
+    <queries>
+        <package android:name="com.android.microdroid.vmshare_app" />
+    </queries>
     <application>
     </application>
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
diff --git a/tests/testapk/AndroidTest.xml b/tests/testapk/AndroidTest.xml
index 787ebd4..929dd31 100644
--- a/tests/testapk/AndroidTest.xml
+++ b/tests/testapk/AndroidTest.xml
@@ -21,6 +21,7 @@
     <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="test-file-name" value="MicrodroidTestApp.apk" />
+        <option name="test-file-name" value="MicrodroidVmShareApp.apk" />
     </target_preparer>
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="com.android.microdroid.test" />
diff --git a/tests/testapk/assets/vm_config_crash.json b/tests/testapk/assets/vm_config_crash.json
index 3ec34a3..ce6af80 100644
--- a/tests/testapk/assets/vm_config_crash.json
+++ b/tests/testapk/assets/vm_config_crash.json
@@ -1,10 +1,9 @@
 {
-    "os": {
-      "name": "microdroid"
-    },
-    "task": {
-      "type": "microdroid_launcher",
-      "command": "MicrodroidIdleNativeLib.so"
-    },
-    "export_tombstones": true
+  "os": {
+    "name": "microdroid"
+  },
+  "task": {
+    "type": "microdroid_launcher",
+    "command": "MicrodroidCrashNativeLib.so"
   }
+}
diff --git a/tests/testapk/assets/vm_config_crash_no_tombstone.json b/tests/testapk/assets/vm_config_crash_no_tombstone.json
deleted file mode 100644
index 9678e38..0000000
--- a/tests/testapk/assets/vm_config_crash_no_tombstone.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
-    "os": {
-      "name": "microdroid"
-    },
-    "task": {
-      "type": "microdroid_launcher",
-      "command": "MicrodroidIdleNativeLib.so"
-    },
-    "export_tombstones": false
-  }
diff --git a/tests/testapk/assets/vm_config_no_tombstone.json b/tests/testapk/assets/vm_config_no_tombstone.json
new file mode 100644
index 0000000..97e764d
--- /dev/null
+++ b/tests/testapk/assets/vm_config_no_tombstone.json
@@ -0,0 +1,10 @@
+{
+  "os": {
+    "name": "microdroid"
+  },
+  "task": {
+    "type": "microdroid_launcher",
+    "command": "MicrodroidTestNativeLib.so"
+  },
+  "export_tombstones": false
+}
diff --git a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
index 9cafd68..542f595 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -18,25 +18,35 @@
 import static android.system.virtualmachine.VirtualMachine.STATUS_DELETED;
 import static android.system.virtualmachine.VirtualMachine.STATUS_RUNNING;
 import static android.system.virtualmachine.VirtualMachine.STATUS_STOPPED;
+import static android.system.virtualmachine.VirtualMachineConfig.CPU_TOPOLOGY_MATCH_HOST;
+import static android.system.virtualmachine.VirtualMachineConfig.CPU_TOPOLOGY_ONE_CPU;
 import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_FULL;
 import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_NONE;
 import static android.system.virtualmachine.VirtualMachineManager.CAPABILITY_NON_PROTECTED_VM;
 import static android.system.virtualmachine.VirtualMachineManager.CAPABILITY_PROTECTED_VM;
-
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.TruthJUnit.assume;
-
+import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
+import static org.junit.Assume.assumeFalse;
 import static org.junit.Assert.assertThrows;
 
-import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
+import com.google.common.base.Strings;
+import com.google.common.truth.BooleanSubject;
 
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.ContextWrapper;
+import android.content.Intent;
+import android.content.ServiceConnection;
 import android.os.Build;
+import android.os.IBinder;
+import android.os.Parcel;
 import android.os.ParcelFileDescriptor;
 import android.os.ParcelFileDescriptor.AutoCloseInputStream;
 import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
 import android.os.SystemProperties;
+import android.system.OsConstants;
 import android.system.virtualmachine.VirtualMachine;
 import android.system.virtualmachine.VirtualMachineCallback;
 import android.system.virtualmachine.VirtualMachineConfig;
@@ -45,14 +55,12 @@
 import android.system.virtualmachine.VirtualMachineManager;
 import android.util.Log;
 
-import androidx.test.core.app.ApplicationProvider;
-
 import com.android.compatibility.common.util.CddTest;
 import com.android.microdroid.test.device.MicrodroidDeviceTestBase;
+import com.android.microdroid.test.vmshare.IVmShareTestService;
+import com.android.microdroid.testservice.IAppCallback;
 import com.android.microdroid.testservice.ITestService;
-
-import com.google.common.base.Strings;
-import com.google.common.truth.BooleanSubject;
+import com.android.microdroid.testservice.IVmCallback;
 
 import org.junit.After;
 import org.junit.Before;
@@ -85,6 +93,8 @@
 import java.util.OptionalLong;
 import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
 
 import co.nstant.in.cbor.CborDecoder;
@@ -125,21 +135,23 @@
     private static final long MIN_MEM_X86_64 = 196 * ONE_MEBI;
     private static final String EXAMPLE_STRING = "Literally any string!! :)";
 
-    @Test
-    @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
-    public void createAndConnectToVm() throws Exception {
-        assumeSupportedKernel();
+    private static final String VM_SHARE_APP_PACKAGE_NAME = "com.android.microdroid.vmshare_app";
+
+    private void createAndConnectToVmHelper(int cpuTopology) throws Exception {
+        assumeSupportedDevice();
 
         VirtualMachineConfig config =
                 newVmConfigBuilder()
                         .setPayloadBinaryName("MicrodroidTestNativeLib.so")
                         .setMemoryBytes(minMemoryRequired())
                         .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .setCpuTopology(cpuTopology)
                         .build();
         VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config);
 
         TestResults testResults =
                 runVmTestService(
+                        TAG,
                         vm,
                         (ts, tr) -> {
                             tr.mAddInteger = ts.addInteger(123, 456);
@@ -148,7 +160,7 @@
                             tr.mApkContentsPath = ts.getApkContentsPath();
                             tr.mEncryptedStoragePath = ts.getEncryptedStoragePath();
                         });
-        assertThat(testResults.mException).isNull();
+        testResults.assertNoException();
         assertThat(testResults.mAddInteger).isEqualTo(123 + 456);
         assertThat(testResults.mAppRunProp).isEqualTo("true");
         assertThat(testResults.mSublibRunProp).isEqualTo("true");
@@ -158,8 +170,20 @@
 
     @Test
     @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
+    public void createAndConnectToVm() throws Exception {
+        createAndConnectToVmHelper(CPU_TOPOLOGY_ONE_CPU);
+    }
+
+    @Test
+    @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
+    public void createAndConnectToVm_HostCpuTopology() throws Exception {
+        createAndConnectToVmHelper(CPU_TOPOLOGY_MATCH_HOST);
+    }
+
+    @Test
+    @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
     public void createAndRunNoDebugVm() throws Exception {
-        assumeSupportedKernel();
+        assumeSupportedDevice();
 
         // For most of our tests we use a debug VM so failures can be diagnosed.
         // But we do need non-debug VMs to work, so run one.
@@ -173,12 +197,8 @@
         VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config);
 
         TestResults testResults =
-                runVmTestService(
-                        vm,
-                        (ts, tr) -> {
-                            tr.mAddInteger = ts.addInteger(37, 73);
-                        });
-        assertThat(testResults.mException).isNull();
+                runVmTestService(TAG, vm, (ts, tr) -> tr.mAddInteger = ts.addInteger(37, 73));
+        testResults.assertNoException();
         assertThat(testResults.mAddInteger).isEqualTo(37 + 73);
     }
 
@@ -190,7 +210,7 @@
                 "9.17/C-1-4",
             })
     public void createVmRequiresPermission() {
-        assumeSupportedKernel();
+        assumeSupportedDevice();
 
         revokePermission(VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION);
 
@@ -209,9 +229,9 @@
     }
 
     @Test
-    @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
+    @CddTest(requirements = {"9.17/C-1-1"})
     public void autoCloseVm() throws Exception {
-        assumeSupportedKernel();
+        assumeSupportedDevice();
 
         VirtualMachineConfig config =
                 newVmConfigBuilder()
@@ -240,9 +260,63 @@
     }
 
     @Test
-    @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
+    @CddTest(requirements = {"9.17/C-1-1"})
+    public void autoCloseVmDescriptor() throws Exception {
+        VirtualMachineConfig config =
+                newVmConfigBuilder()
+                        .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .build();
+        VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config);
+        VirtualMachineDescriptor descriptor = vm.toDescriptor();
+
+        Parcel parcel = Parcel.obtain();
+        try (descriptor) {
+            // It should be ok to use at this point
+            descriptor.writeToParcel(parcel, 0);
+        }
+
+        // But not now - it's been closed.
+        assertThrows(IllegalStateException.class, () -> descriptor.writeToParcel(parcel, 0));
+        assertThrows(
+                IllegalStateException.class,
+                () -> getVirtualMachineManager().importFromDescriptor("imported_vm", descriptor));
+
+        // Closing again is fine.
+        descriptor.close();
+
+        // Tidy up
+        parcel.recycle();
+    }
+
+    @Test
+    @CddTest(requirements = {"9.17/C-1-1"})
+    public void vmDescriptorClosedOnImport() throws Exception {
+        VirtualMachineConfig config =
+                newVmConfigBuilder()
+                        .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .build();
+        VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config);
+        VirtualMachineDescriptor descriptor = vm.toDescriptor();
+
+        getVirtualMachineManager().importFromDescriptor("imported_vm", descriptor);
+        try {
+            // Descriptor has been implicitly closed
+            assertThrows(
+                    IllegalStateException.class,
+                    () ->
+                            getVirtualMachineManager()
+                                    .importFromDescriptor("imported_vm2", descriptor));
+        } finally {
+            getVirtualMachineManager().delete("imported_vm");
+        }
+    }
+
+    @Test
+    @CddTest(requirements = {"9.17/C-1-1"})
     public void vmLifecycleChecks() throws Exception {
-        assumeSupportedKernel();
+        assumeSupportedDevice();
 
         VirtualMachineConfig config =
                 newVmConfigBuilder()
@@ -291,7 +365,7 @@
     @Test
     @CddTest(requirements = {"9.17/C-1-1"})
     public void connectVsock() throws Exception {
-        assumeSupportedKernel();
+        assumeSupportedDevice();
 
         VirtualMachineConfig config =
                 newVmConfigBuilder()
@@ -301,19 +375,15 @@
                         .build();
         VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_vsock", config);
 
-        AtomicReference<Exception> exception = new AtomicReference<>();
         AtomicReference<String> response = new AtomicReference<>();
         String request = "Look not into the abyss";
 
-        VmEventListener listener =
-                new VmEventListener() {
-                    @Override
-                    public void onPayloadReady(VirtualMachine vm) {
-                        try (vm) {
-                            ITestService testService =
-                                    ITestService.Stub.asInterface(
-                                            vm.connectToVsockServer(ITestService.SERVICE_PORT));
-                            testService.runEchoReverseServer();
+        TestResults testResults =
+                runVmTestService(
+                        TAG,
+                        vm,
+                        (service, results) -> {
+                            service.runEchoReverseServer();
 
                             ParcelFileDescriptor pfd =
                                     vm.connectVsock(ITestService.ECHO_REVERSE_PORT);
@@ -326,16 +396,62 @@
                                 writer.flush();
                                 response.set(reader.readLine());
                             }
-                        } catch (Exception e) {
-                            exception.set(e);
-                        }
+                        });
+        testResults.assertNoException();
+        assertThat(response.get()).isEqualTo(new StringBuilder(request).reverse().toString());
+    }
+
+    @Test
+    @CddTest(requirements = {"9.17/C-1-1"})
+    public void binderCallbacksWork() throws Exception {
+        assumeSupportedDevice();
+
+        VirtualMachineConfig config =
+                newVmConfigBuilder()
+                        .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+                        .setMemoryBytes(minMemoryRequired())
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .build();
+        VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config);
+
+        String request = "Hello";
+        CompletableFuture<String> response = new CompletableFuture<>();
+
+        IAppCallback appCallback =
+                new IAppCallback.Stub() {
+                    @Override
+                    public void setVmCallback(IVmCallback vmCallback) {
+                        // Do this on a separate thread to simulate an asynchronous trigger,
+                        // and to make sure it doesn't happen in the context of an inbound binder
+                        // call.
+                        new Thread() {
+                            @Override
+                            public void run() {
+                                try {
+                                    vmCallback.echoMessage(request);
+                                } catch (Exception e) {
+                                    response.completeExceptionally(e);
+                                }
+                            }
+                        }.start();
+                    }
+
+                    @Override
+                    public void onEchoRequestReceived(String message) {
+                        response.complete(message);
                     }
                 };
-        listener.runToFinish(TAG, vm);
-        if (exception.get() != null) {
-            throw new RuntimeException(exception.get());
-        }
-        assertThat(response.get()).isEqualTo(new StringBuilder(request).reverse().toString());
+
+        TestResults testResults =
+                runVmTestService(
+                        TAG,
+                        vm,
+                        (service, results) -> {
+                            service.requestCallback(appCallback);
+                            response.get(10, TimeUnit.SECONDS);
+                        });
+        testResults.assertNoException();
+        assertThat(response.getNow("no response")).isEqualTo("Received: " + request);
     }
 
     @Test
@@ -348,7 +464,7 @@
         assertThat(minimal.getApkPath()).isNull();
         assertThat(minimal.getDebugLevel()).isEqualTo(DEBUG_LEVEL_NONE);
         assertThat(minimal.getMemoryBytes()).isEqualTo(0);
-        assertThat(minimal.getNumCpus()).isEqualTo(1);
+        assertThat(minimal.getCpuTopology()).isEqualTo(CPU_TOPOLOGY_ONE_CPU);
         assertThat(minimal.getPayloadBinaryName()).isEqualTo("binary.so");
         assertThat(minimal.getPayloadConfigPath()).isNull();
         assertThat(minimal.isProtectedVm()).isEqualTo(isProtectedVm());
@@ -358,15 +474,14 @@
 
         // Maximal has everything that can be set to some non-default value. (And has different
         // values than minimal for the required fields.)
-        int maxCpus = Runtime.getRuntime().availableProcessors();
         VirtualMachineConfig.Builder maximalBuilder =
                 new VirtualMachineConfig.Builder(getContext())
                         .setProtectedVm(mProtectedVm)
                         .setPayloadConfigPath("config/path")
                         .setApkPath("/apk/path")
-                        .setNumCpus(maxCpus)
                         .setDebugLevel(DEBUG_LEVEL_FULL)
                         .setMemoryBytes(42)
+                        .setCpuTopology(CPU_TOPOLOGY_MATCH_HOST)
                         .setEncryptedStorageBytes(1_000_000)
                         .setVmOutputCaptured(true);
         VirtualMachineConfig maximal = maximalBuilder.build();
@@ -374,7 +489,7 @@
         assertThat(maximal.getApkPath()).isEqualTo("/apk/path");
         assertThat(maximal.getDebugLevel()).isEqualTo(DEBUG_LEVEL_FULL);
         assertThat(maximal.getMemoryBytes()).isEqualTo(42);
-        assertThat(maximal.getNumCpus()).isEqualTo(maxCpus);
+        assertThat(maximal.getCpuTopology()).isEqualTo(CPU_TOPOLOGY_MATCH_HOST);
         assertThat(maximal.getPayloadBinaryName()).isNull();
         assertThat(maximal.getPayloadConfigPath()).isEqualTo("config/path");
         assertThat(maximal.isProtectedVm()).isEqualTo(isProtectedVm());
@@ -406,7 +521,7 @@
                 IllegalArgumentException.class, () -> builder.setPayloadBinaryName("dir/file.so"));
         assertThrows(IllegalArgumentException.class, () -> builder.setDebugLevel(-1));
         assertThrows(IllegalArgumentException.class, () -> builder.setMemoryBytes(0));
-        assertThrows(IllegalArgumentException.class, () -> builder.setNumCpus(0));
+        assertThrows(IllegalArgumentException.class, () -> builder.setCpuTopology(-1));
         assertThrows(IllegalArgumentException.class, () -> builder.setEncryptedStorageBytes(0));
 
         // Consistency checks enforced at build time.
@@ -431,8 +546,6 @@
     @Test
     @CddTest(requirements = {"9.17/C-1-1"})
     public void compatibleConfigTests() {
-        int maxCpus = Runtime.getRuntime().availableProcessors();
-
         VirtualMachineConfig baseline = newBaselineBuilder().build();
 
         // A config must be compatible with itself
@@ -440,9 +553,9 @@
 
         // Changes that must always be compatible
         assertConfigCompatible(baseline, newBaselineBuilder().setMemoryBytes(99)).isTrue();
-        if (maxCpus > 1) {
-            assertConfigCompatible(baseline, newBaselineBuilder().setNumCpus(2)).isTrue();
-        }
+        assertConfigCompatible(
+                        baseline, newBaselineBuilder().setCpuTopology(CPU_TOPOLOGY_MATCH_HOST))
+                .isTrue();
 
         // Changes that must be incompatible, since they must change the VM identity.
         assertConfigCompatible(baseline, newBaselineBuilder().setDebugLevel(DEBUG_LEVEL_FULL))
@@ -523,7 +636,7 @@
     @Test
     @CddTest(requirements = {"9.17/C-1-1"})
     public void vmmGetAndCreate() throws Exception {
-        assumeSupportedKernel();
+        assumeSupportedDevice();
 
         VirtualMachineConfig config =
                 newVmConfigBuilder()
@@ -621,7 +734,7 @@
             "9.17/C-1-4",
     })
     public void createVmWithConfigRequiresPermission() throws Exception {
-        assumeSupportedKernel();
+        assumeSupportedDevice();
 
         VirtualMachineConfig config =
                 newVmConfigBuilder()
@@ -633,7 +746,8 @@
                 forceCreateNewVirtualMachine("test_vm_config_requires_permission", config);
 
         SecurityException e =
-                assertThrows(SecurityException.class, () -> runVmTestService(vm, (ts, tr) -> {}));
+                assertThrows(
+                        SecurityException.class, () -> runVmTestService(TAG, vm, (ts, tr) -> {}));
         assertThat(e).hasMessageThat()
                 .contains("android.permission.USE_CUSTOM_VIRTUAL_MACHINE permission");
     }
@@ -643,7 +757,7 @@
             "9.17/C-1-1",
     })
     public void deleteVm() throws Exception {
-        assumeSupportedKernel();
+        assumeSupportedDevice();
 
         VirtualMachineConfig config =
                 newVmConfigBuilder()
@@ -671,7 +785,7 @@
                 "9.17/C-1-1",
             })
     public void deleteVmFiles() throws Exception {
-        assumeSupportedKernel();
+        assumeSupportedDevice();
 
         VirtualMachineConfig config =
                 newVmConfigBuilder()
@@ -704,7 +818,7 @@
             "9.17/C-1-1",
     })
     public void validApkPathIsAccepted() throws Exception {
-        assumeSupportedKernel();
+        assumeSupportedDevice();
 
         VirtualMachineConfig config =
                 newVmConfigBuilder()
@@ -718,11 +832,12 @@
 
         TestResults testResults =
                 runVmTestService(
+                        TAG,
                         vm,
                         (ts, tr) -> {
                             tr.mApkContentsPath = ts.getApkContentsPath();
                         });
-        assertThat(testResults.mException).isNull();
+        testResults.assertNoException();
         assertThat(testResults.mApkContentsPath).isEqualTo("/mnt/apk");
     }
 
@@ -740,7 +855,7 @@
             "9.17/C-2-1"
     })
     public void extraApk() throws Exception {
-        assumeSupportedKernel();
+        assumeSupportedDevice();
 
         grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
         VirtualMachineConfig config =
@@ -753,6 +868,7 @@
 
         TestResults testResults =
                 runVmTestService(
+                        TAG,
                         vm,
                         (ts, tr) -> {
                             tr.mExtraApkTestProp =
@@ -807,7 +923,7 @@
     }
 
     private void changeDebugLevel(int fromLevel, int toLevel) throws Exception {
-        assumeSupportedKernel();
+        assumeSupportedDevice();
 
         VirtualMachineConfig.Builder builder =
                 newVmConfigBuilder()
@@ -845,15 +961,16 @@
 
     private VmCdis launchVmAndGetCdis(String instanceName) throws Exception {
         VirtualMachine vm = getVirtualMachineManager().get(instanceName);
-        final VmCdis vmCdis = new VmCdis();
-        final CompletableFuture<Exception> exception = new CompletableFuture<>();
+        VmCdis vmCdis = new VmCdis();
+        CompletableFuture<Exception> exception = new CompletableFuture<>();
         VmEventListener listener =
                 new VmEventListener() {
                     @Override
                     public void onPayloadReady(VirtualMachine vm) {
                         try {
-                            ITestService testService = ITestService.Stub.asInterface(
-                                    vm.connectToVsockServer(ITestService.SERVICE_PORT));
+                            ITestService testService =
+                                    ITestService.Stub.asInterface(
+                                            vm.connectToVsockServer(ITestService.SERVICE_PORT));
                             vmCdis.cdiAttest = testService.insecurelyExposeAttestationCdi();
                             vmCdis.instanceSecret = testService.insecurelyExposeVmInstanceSecret();
                         } catch (Exception e) {
@@ -877,7 +994,7 @@
             "9.17/C-2-7"
     })
     public void instancesOfSameVmHaveDifferentCdis() throws Exception {
-        assumeSupportedKernel();
+        assumeSupportedDevice();
 
         grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
         VirtualMachineConfig normalConfig =
@@ -903,7 +1020,7 @@
             "9.17/C-2-7"
     })
     public void sameInstanceKeepsSameCdis() throws Exception {
-        assumeSupportedKernel();
+        assumeSupportedDevice();
         assume().withMessage("Skip on CF. Too Slow. b/257270529").that(isCuttlefish()).isFalse();
 
         grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
@@ -928,7 +1045,7 @@
             "9.17/C-2-7"
     })
     public void bccIsSuperficiallyWellFormed() throws Exception {
-        assumeSupportedKernel();
+        assumeSupportedDevice();
 
         grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
         VirtualMachineConfig normalConfig =
@@ -937,26 +1054,15 @@
                         .setDebugLevel(DEBUG_LEVEL_FULL)
                         .build();
         VirtualMachine vm = forceCreateNewVirtualMachine("bcc_vm", normalConfig);
-        final CompletableFuture<byte[]> bcc = new CompletableFuture<>();
-        final CompletableFuture<Exception> exception = new CompletableFuture<>();
-        VmEventListener listener =
-                new VmEventListener() {
-                    @Override
-                    public void onPayloadReady(VirtualMachine vm) {
-                        try {
-                            ITestService testService = ITestService.Stub.asInterface(
-                                    vm.connectToVsockServer(ITestService.SERVICE_PORT));
-                            bcc.complete(testService.getBcc());
-                        } catch (Exception e) {
-                            exception.complete(e);
-                        } finally {
-                            forceStop(vm);
-                        }
-                    }
-                };
-        listener.runToFinish(TAG, vm);
-        byte[] bccBytes = bcc.getNow(null);
-        assertThat(exception.getNow(null)).isNull();
+        TestResults testResults =
+                runVmTestService(
+                        TAG,
+                        vm,
+                        (service, results) -> {
+                            results.mBcc = service.getBcc();
+                        });
+        testResults.assertNoException();
+        byte[] bccBytes = testResults.mBcc;
         assertThat(bccBytes).isNotNull();
 
         ByteArrayInputStream bais = new ByteArrayInputStream(bccBytes);
@@ -978,7 +1084,7 @@
             "9.17/C-1-2"
     })
     public void accessToCdisIsRestricted() throws Exception {
-        assumeSupportedKernel();
+        assumeSupportedDevice();
 
         VirtualMachineConfig config =
                 newVmConfigBuilder()
@@ -993,10 +1099,6 @@
 
     private static final UUID MICRODROID_PARTITION_UUID =
             UUID.fromString("cf9afe9a-0662-11ec-a329-c32663a09d75");
-    private static final UUID U_BOOT_AVB_PARTITION_UUID =
-            UUID.fromString("7e8221e7-03e6-4969-948b-73a4c809a4f2");
-    private static final UUID U_BOOT_ENV_PARTITION_UUID =
-            UUID.fromString("0ab72d30-86ae-4d05-81b2-c1760be2b1f9");
     private static final UUID PVM_FW_PARTITION_UUID =
             UUID.fromString("90d2174a-038a-4bc6-adf3-824848fc5825");
     private static final long BLOCK_SIZE = 512;
@@ -1182,7 +1284,7 @@
 
     @Test
     public void importedVmAndOriginalVmHaveTheSameCdi() throws Exception {
-        assumeSupportedKernel();
+        assumeSupportedDevice();
         // Arrange
         grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
         VirtualMachineConfig config =
@@ -1195,7 +1297,6 @@
         VirtualMachine vmOrig = forceCreateNewVirtualMachine(vmNameOrig, config);
         VmCdis origCdis = launchVmAndGetCdis(vmNameOrig);
         assertThat(origCdis.instanceSecret).isNotNull();
-        VirtualMachineDescriptor descriptor = vmOrig.toDescriptor();
         VirtualMachineManager vmm = getVirtualMachineManager();
         if (vmm.get(vmNameImport) != null) {
             vmm.delete(vmNameImport);
@@ -1203,7 +1304,7 @@
 
         // Action
         // The imported VM will be fetched by name later.
-        VirtualMachine unusedVmImport = vmm.importFromDescriptor(vmNameImport, descriptor);
+        vmm.importFromDescriptor(vmNameImport, vmOrig.toDescriptor());
 
         // Asserts
         VmCdis importCdis = launchVmAndGetCdis(vmNameImport);
@@ -1211,14 +1312,14 @@
     }
 
     @Test
-    @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
+    @CddTest(requirements = {"9.17/C-1-1"})
     public void importedVmIsEqualToTheOriginalVm_WithoutStorage() throws Exception {
         TestResults testResults = importedVmIsEqualToTheOriginalVm(false);
         assertThat(testResults.mEncryptedStoragePath).isEqualTo("");
     }
 
     @Test
-    @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
+    @CddTest(requirements = {"9.17/C-1-1"})
     public void importedVmIsEqualToTheOriginalVm_WithStorage() throws Exception {
         TestResults testResults = importedVmIsEqualToTheOriginalVm(true);
         assertThat(testResults.mEncryptedStoragePath).isEqualTo("/mnt/encryptedstore");
@@ -1241,21 +1342,21 @@
         // Run something to make the instance.img different with the initialized one.
         TestResults origTestResults =
                 runVmTestService(
+                        TAG,
                         vmOrig,
                         (ts, tr) -> {
                             tr.mAddInteger = ts.addInteger(123, 456);
                             tr.mEncryptedStoragePath = ts.getEncryptedStoragePath();
                         });
-        assertThat(origTestResults.mException).isNull();
+        origTestResults.assertNoException();
         assertThat(origTestResults.mAddInteger).isEqualTo(123 + 456);
-        VirtualMachineDescriptor descriptor = vmOrig.toDescriptor();
         VirtualMachineManager vmm = getVirtualMachineManager();
         if (vmm.get(vmNameImport) != null) {
             vmm.delete(vmNameImport);
         }
 
         // Action
-        VirtualMachine vmImport = vmm.importFromDescriptor(vmNameImport, descriptor);
+        VirtualMachine vmImport = vmm.importFromDescriptor(vmNameImport, vmOrig.toDescriptor());
 
         // Asserts
         assertFileContentsAreEqualInTwoVms("config.xml", vmNameOrig, vmNameImport);
@@ -1268,20 +1369,21 @@
         assertThat(vmImport).isEqualTo(vmm.get(vmNameImport));
         TestResults testResults =
                 runVmTestService(
+                        TAG,
                         vmImport,
                         (ts, tr) -> {
                             tr.mAddInteger = ts.addInteger(123, 456);
                             tr.mEncryptedStoragePath = ts.getEncryptedStoragePath();
                         });
-        assertThat(testResults.mException).isNull();
+        testResults.assertNoException();
         assertThat(testResults.mAddInteger).isEqualTo(123 + 456);
         return testResults;
     }
 
     @Test
-    @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
+    @CddTest(requirements = {"9.17/C-1-1"})
     public void encryptedStorageAvailable() throws Exception {
-        assumeSupportedKernel();
+        assumeSupportedDevice();
 
         VirtualMachineConfig config =
                 newVmConfigBuilder()
@@ -1294,6 +1396,7 @@
 
         TestResults testResults =
                 runVmTestService(
+                        TAG,
                         vm,
                         (ts, tr) -> {
                             tr.mEncryptedStoragePath = ts.getEncryptedStoragePath();
@@ -1302,9 +1405,70 @@
     }
 
     @Test
+    @CddTest(requirements = {"9.17/C-1-1"})
+    public void encryptedStorageIsInaccessibleToDifferentVm() throws Exception {
+        assumeSupportedDevice();
+
+        VirtualMachineConfig config =
+                newVmConfigBuilder()
+                        .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+                        .setMemoryBytes(minMemoryRequired())
+                        .setEncryptedStorageBytes(4_000_000)
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .build();
+
+        VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config);
+
+        TestResults testResults =
+                runVmTestService(
+                        TAG,
+                        vm,
+                        (ts, tr) -> {
+                            ts.writeToFile(
+                                    /* content= */ EXAMPLE_STRING,
+                                    /* path= */ "/mnt/encryptedstore/test_file");
+                        });
+        testResults.assertNoException();
+
+        // Start a different vm (this changes the vm identity)
+        VirtualMachine diff_test_vm = forceCreateNewVirtualMachine("diff_test_vm", config);
+
+        // Replace the backing storage image to the original one
+        File storageImgOrig = getVmFile("test_vm", "storage.img");
+        File storageImgNew = getVmFile("diff_test_vm", "storage.img");
+        Files.copy(storageImgOrig.toPath(), storageImgNew.toPath(), REPLACE_EXISTING);
+        assertFileContentsAreEqualInTwoVms("storage.img", "test_vm", "diff_test_vm");
+
+        CompletableFuture<Boolean> onPayloadReadyExecuted = new CompletableFuture<>();
+        CompletableFuture<Boolean> onErrorExecuted = new CompletableFuture<>();
+        CompletableFuture<String> errorMessage = new CompletableFuture<>();
+        VmEventListener listener =
+                new VmEventListener() {
+                    @Override
+                    public void onPayloadReady(VirtualMachine vm) {
+                        onPayloadReadyExecuted.complete(true);
+                        super.onPayloadReady(vm);
+                    }
+
+                    @Override
+                    public void onError(VirtualMachine vm, int errorCode, String message) {
+                        onErrorExecuted.complete(true);
+                        errorMessage.complete(message);
+                        super.onError(vm, errorCode, message);
+                    }
+                };
+        listener.runToFinish(TAG, diff_test_vm);
+
+        // Assert that payload never started & error message reflects storage error.
+        assertThat(onPayloadReadyExecuted.getNow(false)).isFalse();
+        assertThat(onErrorExecuted.getNow(false)).isTrue();
+        assertThat(errorMessage.getNow("")).contains("Unable to prepare encrypted storage");
+    }
+
+    @Test
     @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
     public void microdroidLauncherHasEmptyCapabilities() throws Exception {
-        assumeSupportedKernel();
+        assumeSupportedDevice();
 
         final VirtualMachineConfig vmConfig =
                 newVmConfigBuilder()
@@ -1316,19 +1480,20 @@
 
         final TestResults testResults =
                 runVmTestService(
+                        TAG,
                         vm,
                         (ts, tr) -> {
                             tr.mEffectiveCapabilities = ts.getEffectiveCapabilities();
                         });
 
-        assertThat(testResults.mException).isNull();
+        testResults.assertNoException();
         assertThat(testResults.mEffectiveCapabilities).isEmpty();
     }
 
     @Test
-    @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
+    @CddTest(requirements = {"9.17/C-1-1"})
     public void encryptedStorageIsPersistent() throws Exception {
-        assumeSupportedKernel();
+        assumeSupportedDevice();
 
         VirtualMachineConfig config =
                 newVmConfigBuilder()
@@ -1340,30 +1505,32 @@
         VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_a", config);
         TestResults testResults =
                 runVmTestService(
+                        TAG,
                         vm,
                         (ts, tr) -> {
                             ts.writeToFile(
                                     /* content= */ EXAMPLE_STRING,
                                     /* path= */ "/mnt/encryptedstore/test_file");
                         });
-        assertThat(testResults.mException).isNull();
+        testResults.assertNoException();
 
         // Re-run the same VM & verify the file persisted. Note, the previous `runVmTestService`
         // stopped the VM
         testResults =
                 runVmTestService(
+                        TAG,
                         vm,
                         (ts, tr) -> {
                             tr.mFileContent = ts.readFromFile("/mnt/encryptedstore/test_file");
                         });
-        assertThat(testResults.mException).isNull();
+        testResults.assertNoException();
         assertThat(testResults.mFileContent).isEqualTo(EXAMPLE_STRING);
     }
 
     @Test
     @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
     public void canReadFileFromAssets_debugFull() throws Exception {
-        assumeSupportedKernel();
+        assumeSupportedDevice();
 
         VirtualMachineConfig config =
                 newVmConfigBuilder()
@@ -1375,21 +1542,22 @@
 
         TestResults testResults =
                 runVmTestService(
+                        TAG,
                         vm,
                         (testService, ts) -> {
                             ts.mFileContent = testService.readFromFile("/mnt/apk/assets/file.txt");
                         });
 
-        assertThat(testResults.mException).isNull();
+        testResults.assertNoException();
         assertThat(testResults.mFileContent).isEqualTo("Hello, I am a file!");
     }
 
     @Test
     public void outputShouldBeExplicitlyCaptured() throws Exception {
-        assumeSupportedKernel();
+        assumeSupportedDevice();
 
         final VirtualMachineConfig vmConfig =
-                new VirtualMachineConfig.Builder(ApplicationProvider.getApplicationContext())
+                new VirtualMachineConfig.Builder(getContext())
                         .setProtectedVm(mProtectedVm)
                         .setPayloadBinaryName("MicrodroidTestNativeLib.so")
                         .setDebugLevel(DEBUG_LEVEL_FULL)
@@ -1407,11 +1575,27 @@
         }
     }
 
+    private boolean isConsoleOutputEnabledByDebugPolicy() {
+        if (isUserBuild()) {
+            Log.i(
+                    TAG,
+                    "Debug policy is inaccessible in user build. Assumes that console output is"
+                            + " disabled");
+            return false;
+        }
+        try {
+            return getDebugPolicyBoolean("/avf/guest/common/log");
+        } catch (IOException e) {
+            Log.w(TAG, "Fail to read debug policy. Assumes false", e);
+            return false;
+        }
+    }
+
     private boolean checkVmOutputIsRedirectedToLogcat(boolean debuggable) throws Exception {
         String time =
                 LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"));
         final VirtualMachineConfig vmConfig =
-                new VirtualMachineConfig.Builder(ApplicationProvider.getApplicationContext())
+                new VirtualMachineConfig.Builder(getContext())
                         .setProtectedVm(mProtectedVm)
                         .setPayloadBinaryName("MicrodroidTestNativeLib.so")
                         .setDebugLevel(debuggable ? DEBUG_LEVEL_FULL : DEBUG_LEVEL_NONE)
@@ -1419,14 +1603,7 @@
                         .build();
         final VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_logcat", vmConfig);
 
-        VmEventListener listener =
-                new VmEventListener() {
-                    @Override
-                    public void onPayloadStarted(VirtualMachine vm) {
-                        forceStop(vm);
-                    }
-                };
-        listener.runToFinish(TAG, vm);
+        runVmTestService(TAG, vm, (service, results) -> {});
 
         // only check logs printed after this test
         Process logcatProcess =
@@ -1446,18 +1623,364 @@
 
     @Test
     public void outputIsRedirectedToLogcatIfNotCaptured() throws Exception {
-        assumeSupportedKernel();
+        assumeSupportedDevice();
+        assumeFalse(
+                "Debug policy would turn on console output. Perhaps userdebug build?",
+                isConsoleOutputEnabledByDebugPolicy());
 
         assertThat(checkVmOutputIsRedirectedToLogcat(true)).isTrue();
     }
 
     @Test
     public void outputIsNotRedirectedToLogcatIfNotDebuggable() throws Exception {
-        assumeSupportedKernel();
+        assumeSupportedDevice();
+        assumeFalse(
+                "Debug policy would turn on console output. Perhaps userdebug build?",
+                isConsoleOutputEnabledByDebugPolicy());
 
         assertThat(checkVmOutputIsRedirectedToLogcat(false)).isFalse();
     }
 
+    @Test
+    public void testStartVmWithPayloadOfAnotherApp() throws Exception {
+        assumeSupportedDevice();
+
+        Context ctx = getContext();
+        Context otherAppCtx = ctx.createPackageContext(VM_SHARE_APP_PACKAGE_NAME, 0);
+
+        VirtualMachineConfig config =
+                new VirtualMachineConfig.Builder(otherAppCtx)
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .setProtectedVm(isProtectedVm())
+                        .setPayloadBinaryName("MicrodroidPayloadInOtherAppNativeLib.so")
+                        .build();
+
+        try (VirtualMachine vm = forceCreateNewVirtualMachine("vm_from_another_app", config)) {
+            TestResults results =
+                    runVmTestService(
+                            TAG,
+                            vm,
+                            (ts, tr) -> {
+                                tr.mAddInteger = ts.addInteger(101, 303);
+                            });
+            assertThat(results.mAddInteger).isEqualTo(404);
+        }
+
+        getVirtualMachineManager().delete("vm_from_another_app");
+    }
+
+    @Test
+    public void testVmDescriptorParcelUnparcel_noTrustedStorage() throws Exception {
+        assumeSupportedDevice();
+
+        VirtualMachineConfig config =
+                newVmConfigBuilder()
+                        .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .build();
+
+        VirtualMachine originalVm = forceCreateNewVirtualMachine("original_vm", config);
+        // Just start & stop the VM.
+        runVmTestService(TAG, originalVm, (ts, tr) -> {});
+
+        // Now create the descriptor and manually parcel & unparcel it.
+        VirtualMachineDescriptor vmDescriptor = toParcelFromParcel(originalVm.toDescriptor());
+
+        if (getVirtualMachineManager().get("import_vm_from_unparceled") != null) {
+            getVirtualMachineManager().delete("import_vm_from_unparceled");
+        }
+
+        VirtualMachine importVm =
+                getVirtualMachineManager()
+                        .importFromDescriptor("import_vm_from_unparceled", vmDescriptor);
+
+        assertFileContentsAreEqualInTwoVms(
+                "config.xml", "original_vm", "import_vm_from_unparceled");
+        assertFileContentsAreEqualInTwoVms(
+                "instance.img", "original_vm", "import_vm_from_unparceled");
+
+        // Check that we can start and stop imported vm as well
+        runVmTestService(TAG, importVm, (ts, tr) -> {});
+    }
+
+    @Test
+    public void testVmDescriptorParcelUnparcel_withTrustedStorage() throws Exception {
+        assumeSupportedDevice();
+
+        VirtualMachineConfig config =
+                newVmConfigBuilder()
+                        .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .setEncryptedStorageBytes(1_000_000)
+                        .build();
+
+        VirtualMachine originalVm = forceCreateNewVirtualMachine("original_vm", config);
+        // Just start & stop the VM.
+        {
+            TestResults testResults =
+                    runVmTestService(
+                            TAG,
+                            originalVm,
+                            (ts, tr) -> {
+                                ts.writeToFile("not a secret!", "/mnt/encryptedstore/secret.txt");
+                            });
+            assertThat(testResults.mException).isNull();
+        }
+
+        // Now create the descriptor and manually parcel & unparcel it.
+        VirtualMachineDescriptor vmDescriptor = toParcelFromParcel(originalVm.toDescriptor());
+
+        if (getVirtualMachineManager().get("import_vm_from_unparceled") != null) {
+            getVirtualMachineManager().delete("import_vm_from_unparceled");
+        }
+
+        VirtualMachine importVm =
+                getVirtualMachineManager()
+                        .importFromDescriptor("import_vm_from_unparceled", vmDescriptor);
+
+        assertFileContentsAreEqualInTwoVms(
+                "config.xml", "original_vm", "import_vm_from_unparceled");
+        assertFileContentsAreEqualInTwoVms(
+                "instance.img", "original_vm", "import_vm_from_unparceled");
+        assertFileContentsAreEqualInTwoVms(
+                "storage.img", "original_vm", "import_vm_from_unparceled");
+
+        TestResults testResults =
+                runVmTestService(
+                        TAG,
+                        importVm,
+                        (ts, tr) -> {
+                            tr.mFileContent = ts.readFromFile("/mnt/encryptedstore/secret.txt");
+                        });
+
+        assertThat(testResults.mException).isNull();
+        assertThat(testResults.mFileContent).isEqualTo("not a secret!");
+    }
+
+    @Test
+    public void testShareVmWithAnotherApp() throws Exception {
+        assumeSupportedDevice();
+
+        Context ctx = getContext();
+        Context otherAppCtx = ctx.createPackageContext(VM_SHARE_APP_PACKAGE_NAME, 0);
+
+        VirtualMachineConfig config =
+                new VirtualMachineConfig.Builder(otherAppCtx)
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .setProtectedVm(isProtectedVm())
+                        .setPayloadBinaryName("MicrodroidPayloadInOtherAppNativeLib.so")
+                        .build();
+
+        VirtualMachine vm = forceCreateNewVirtualMachine("vm_to_share", config);
+        // Just start & stop the VM.
+        runVmTestService(TAG, vm, (ts, tr) -> {});
+        // Get a descriptor that we will share with another app (VM_SHARE_APP_PACKAGE_NAME)
+        VirtualMachineDescriptor vmDesc = vm.toDescriptor();
+
+        Intent serviceIntent = new Intent();
+        serviceIntent.setComponent(
+                new ComponentName(
+                        VM_SHARE_APP_PACKAGE_NAME,
+                        "com.android.microdroid.test.sharevm.VmShareServiceImpl"));
+        serviceIntent.setAction("com.android.microdroid.test.sharevm.VmShareService");
+
+        VmShareServiceConnection connection = new VmShareServiceConnection();
+        boolean ret = ctx.bindService(serviceIntent, connection, Context.BIND_AUTO_CREATE);
+        assertWithMessage("Failed to bind to " + serviceIntent).that(ret).isTrue();
+
+        IVmShareTestService service = connection.waitForService();
+        assertWithMessage("Timed out connecting to " + serviceIntent).that(service).isNotNull();
+
+        try {
+            // Send the VM descriptor to the other app. When received, it will reconstruct the VM
+            // from the descriptor, start it, connect to the ITestService in it, creates a "proxy"
+            // ITestService binder that delegates all the calls to the VM, and share it with this
+            // app. It will allow us to verify assertions on the running VM in the other app.
+            ITestService testServiceProxy = service.startVm(vmDesc);
+
+            int result = testServiceProxy.addInteger(37, 73);
+            assertThat(result).isEqualTo(110);
+        } finally {
+            ctx.unbindService(connection);
+        }
+    }
+
+    @Test
+    public void testShareVmWithAnotherApp_encryptedStorage() throws Exception {
+        assumeSupportedDevice();
+
+        Context ctx = getContext();
+        Context otherAppCtx = ctx.createPackageContext(VM_SHARE_APP_PACKAGE_NAME, 0);
+
+        VirtualMachineConfig config =
+                new VirtualMachineConfig.Builder(otherAppCtx)
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .setProtectedVm(isProtectedVm())
+                        .setEncryptedStorageBytes(3_000_000)
+                        .setPayloadBinaryName("MicrodroidPayloadInOtherAppNativeLib.so")
+                        .build();
+
+        VirtualMachine vm = forceCreateNewVirtualMachine("vm_to_share", config);
+        // Just start & stop the VM.
+        runVmTestService(
+                TAG,
+                vm,
+                (ts, tr) -> {
+                    ts.writeToFile(EXAMPLE_STRING, "/mnt/encryptedstore/private.key");
+                });
+        // Get a descriptor that we will share with another app (VM_SHARE_APP_PACKAGE_NAME)
+        VirtualMachineDescriptor vmDesc = vm.toDescriptor();
+
+        Intent serviceIntent = new Intent();
+        serviceIntent.setComponent(
+                new ComponentName(
+                        VM_SHARE_APP_PACKAGE_NAME,
+                        "com.android.microdroid.test.sharevm.VmShareServiceImpl"));
+        serviceIntent.setAction("com.android.microdroid.test.sharevm.VmShareService");
+
+        VmShareServiceConnection connection = new VmShareServiceConnection();
+        boolean ret = ctx.bindService(serviceIntent, connection, Context.BIND_AUTO_CREATE);
+        assertWithMessage("Failed to bind to " + serviceIntent).that(ret).isTrue();
+
+        IVmShareTestService service = connection.waitForService();
+        assertWithMessage("Timed out connecting to " + serviceIntent).that(service).isNotNull();
+
+        try {
+            // Send the VM descriptor to the other app. When received, it will reconstruct the VM
+            // from the descriptor, start it, connect to the ITestService in it, creates a "proxy"
+            // ITestService binder that delegates all the calls to the VM, and share it with this
+            // app. It will allow us to verify assertions on the running VM in the other app.
+            ITestService testServiceProxy = service.startVm(vmDesc);
+
+            String result = testServiceProxy.readFromFile("/mnt/encryptedstore/private.key");
+            assertThat(result).isEqualTo(EXAMPLE_STRING);
+        } finally {
+            ctx.unbindService(connection);
+        }
+    }
+
+    @Test
+    @CddTest(requirements = {"9.17/C-1-5"})
+    public void testFileUnderBinHasExecutePermission() throws Exception {
+        assumeSupportedDevice();
+
+        VirtualMachineConfig vmConfig =
+                newVmConfigBuilder()
+                        .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+                        .setMemoryBytes(minMemoryRequired())
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .build();
+        VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_perms", vmConfig);
+
+        TestResults testResults =
+                runVmTestService(
+                        TAG,
+                        vm,
+                        (ts, tr) -> {
+                            tr.mFileMode = ts.getFilePermissions("/mnt/apk/bin/measure_io");
+                        });
+
+        testResults.assertNoException();
+        int allPermissionsMask =
+                OsConstants.S_IRUSR
+                        | OsConstants.S_IWUSR
+                        | OsConstants.S_IXUSR
+                        | OsConstants.S_IRGRP
+                        | OsConstants.S_IWGRP
+                        | OsConstants.S_IXGRP
+                        | OsConstants.S_IROTH
+                        | OsConstants.S_IWOTH
+                        | OsConstants.S_IXOTH;
+        assertThat(testResults.mFileMode & allPermissionsMask)
+                .isEqualTo(OsConstants.S_IRUSR | OsConstants.S_IXUSR);
+    }
+
+    // Taken from bionic/libs/kernel/uapi/linux/mounth.h.
+    private static final int MS_NOEXEC = 8;
+
+    @Test
+    @CddTest(requirements = {"9.17/C-1-5"})
+    public void dataIsMountedWithNoExec() throws Exception {
+        assumeSupportedDevice();
+
+        VirtualMachineConfig vmConfig =
+                newVmConfigBuilder()
+                        .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .build();
+        VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_data_mount", vmConfig);
+
+        TestResults testResults =
+                runVmTestService(
+                        TAG,
+                        vm,
+                        (ts, tr) -> {
+                            tr.mMountFlags = ts.getMountFlags("/data");
+                        });
+
+        assertThat(testResults.mException).isNull();
+        assertWithMessage("/data should be mounted with MS_NOEXEC")
+                .that(testResults.mMountFlags & MS_NOEXEC)
+                .isEqualTo(MS_NOEXEC);
+    }
+
+    @Test
+    @CddTest(requirements = {"9.17/C-1-5"})
+    public void encryptedStoreIsMountedWithNoExec() throws Exception {
+        assumeSupportedDevice();
+
+        VirtualMachineConfig vmConfig =
+                newVmConfigBuilder()
+                        .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .setEncryptedStorageBytes(4_000_000)
+                        .build();
+        VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_encstore_no_exec", vmConfig);
+
+        TestResults testResults =
+                runVmTestService(
+                        TAG,
+                        vm,
+                        (ts, tr) -> {
+                            tr.mMountFlags = ts.getMountFlags("/mnt/encryptedstore");
+                        });
+
+        assertThat(testResults.mException).isNull();
+        assertWithMessage("/mnt/encryptedstore should be mounted with MS_NOEXEC")
+                .that(testResults.mMountFlags & MS_NOEXEC)
+                .isEqualTo(MS_NOEXEC);
+    }
+
+    private static class VmShareServiceConnection implements ServiceConnection {
+
+        private final CountDownLatch mLatch = new CountDownLatch(1);
+
+        private IVmShareTestService mVmShareTestService;
+
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            mVmShareTestService = IVmShareTestService.Stub.asInterface(service);
+            mLatch.countDown();
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {}
+
+        private IVmShareTestService waitForService() throws Exception {
+            if (!mLatch.await(1, TimeUnit.MINUTES)) {
+                return null;
+            }
+            return mVmShareTestService;
+        }
+    }
+
+    private VirtualMachineDescriptor toParcelFromParcel(VirtualMachineDescriptor descriptor) {
+        Parcel parcel = Parcel.obtain();
+        descriptor.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        return VirtualMachineDescriptor.CREATOR.createFromParcel(parcel);
+    }
+
     private void assertFileContentsAreEqualInTwoVms(String fileName, String vmName1, String vmName2)
             throws IOException {
         File file1 = getVmFile(vmName1, fileName);
@@ -1469,7 +1992,7 @@
     }
 
     private File getVmFile(String vmName, String fileName) {
-        Context context = ApplicationProvider.getApplicationContext();
+        Context context = getContext();
         Path filePath = Paths.get(context.getDataDir().getPath(), "vm", vmName, fileName);
         return filePath.toFile();
     }
@@ -1497,93 +2020,10 @@
         return 0;
     }
 
-    private void assumeSupportedKernel() {
+    private void assumeSupportedDevice() {
         assume()
                 .withMessage("Skip on 5.4 kernel. b/218303240")
                 .that(KERNEL_VERSION)
                 .isNotEqualTo("5.4");
     }
-
-    static class TestResults {
-        Exception mException;
-        Integer mAddInteger;
-        String mAppRunProp;
-        String mSublibRunProp;
-        String mExtraApkTestProp;
-        String mApkContentsPath;
-        String mEncryptedStoragePath;
-        String[] mEffectiveCapabilities;
-        String mFileContent;
-    }
-
-    private TestResults runVmTestService(VirtualMachine vm, RunTestsAgainstTestService testsToRun)
-            throws Exception {
-        CompletableFuture<Boolean> payloadStarted = new CompletableFuture<>();
-        CompletableFuture<Boolean> payloadReady = new CompletableFuture<>();
-        CompletableFuture<Boolean> payloadFinished = new CompletableFuture<>();
-        TestResults testResults = new TestResults();
-        VmEventListener listener =
-                new VmEventListener() {
-                    ITestService mTestService = null;
-
-                    private void initializeTestService(VirtualMachine vm) {
-                        try {
-                            mTestService =
-                                    ITestService.Stub.asInterface(
-                                            vm.connectToVsockServer(ITestService.SERVICE_PORT));
-                        } catch (Exception e) {
-                            testResults.mException = e;
-                        }
-                    }
-
-                    private void testVMService(VirtualMachine vm) {
-                        try {
-                            if (mTestService == null) initializeTestService(vm);
-                            testsToRun.runTests(mTestService, testResults);
-                        } catch (Exception e) {
-                            testResults.mException = e;
-                        }
-                    }
-
-                    private void quitVMService(VirtualMachine vm) {
-                        try {
-                            mTestService.quit();
-                        } catch (Exception e) {
-                            testResults.mException = e;
-                        }
-                    }
-
-                    @Override
-                    public void onPayloadReady(VirtualMachine vm) {
-                        Log.i(TAG, "onPayloadReady");
-                        payloadReady.complete(true);
-                        testVMService(vm);
-                        quitVMService(vm);
-                    }
-
-                    @Override
-                    public void onPayloadStarted(VirtualMachine vm) {
-                        Log.i(TAG, "onPayloadStarted");
-                        payloadStarted.complete(true);
-                    }
-
-                    @Override
-                    public void onPayloadFinished(VirtualMachine vm, int exitCode) {
-                        Log.i(TAG, "onPayloadFinished: " + exitCode);
-                        payloadFinished.complete(true);
-                        forceStop(vm);
-                    }
-                };
-
-        listener.runToFinish(TAG, vm);
-        assertThat(payloadStarted.getNow(false)).isTrue();
-        assertThat(payloadReady.getNow(false)).isTrue();
-        assertThat(payloadFinished.getNow(false)).isTrue();
-        return testResults;
-    }
-
-    @FunctionalInterface
-    interface RunTestsAgainstTestService {
-        void runTests(ITestService testService, TestResults testResults) throws Exception;
-    }
 }
diff --git a/tests/testapk/src/native/crashbinary.cpp b/tests/testapk/src/native/crashbinary.cpp
new file mode 100644
index 0000000..27f10ec
--- /dev/null
+++ b/tests/testapk/src/native/crashbinary.cpp
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+// A binary killing itself by SIGABRT.
+#include <stdlib.h>
+#include <unistd.h>
+extern "C" int AVmPayload_main() {
+    abort();
+}
diff --git a/tests/testapk/src/native/testbinary.cpp b/tests/testapk/src/native/testbinary.cpp
index 365ea75..d24ddfd 100644
--- a/tests/testapk/src/native/testbinary.cpp
+++ b/tests/testapk/src/native/testbinary.cpp
@@ -15,12 +15,15 @@
  */
 
 #include <aidl/com/android/microdroid/testservice/BnTestService.h>
+#include <aidl/com/android/microdroid/testservice/BnVmCallback.h>
+#include <aidl/com/android/microdroid/testservice/IAppCallback.h>
 #include <android-base/file.h>
 #include <android-base/properties.h>
 #include <android-base/result.h>
 #include <android-base/scopeguard.h>
 #include <android/log.h>
 #include <fcntl.h>
+#include <fstab/fstab.h>
 #include <fsverity_digests.pb.h>
 #include <linux/vm_sockets.h>
 #include <stdint.h>
@@ -40,8 +43,14 @@
 using android::base::make_scope_guard;
 using android::base::Result;
 using android::base::unique_fd;
+using android::fs_mgr::Fstab;
+using android::fs_mgr::FstabEntry;
+using android::fs_mgr::GetEntryForMountPoint;
+using android::fs_mgr::ReadFstabFromFile;
 
 using aidl::com::android::microdroid::testservice::BnTestService;
+using aidl::com::android::microdroid::testservice::BnVmCallback;
+using aidl::com::android::microdroid::testservice::IAppCallback;
 using ndk::ScopedAStatus;
 
 extern void testlib_sub();
@@ -84,28 +93,26 @@
         return ErrnoError() << "Failed to fdopen";
     }
 
-    char* line = nullptr;
-    size_t size = 0;
-    if (getline(&line, &size, input) < 0) {
-        return ErrnoError() << "Failed to read";
+    // Run forever, reverse one line at a time.
+    while (true) {
+        char* line = nullptr;
+        size_t size = 0;
+        if (getline(&line, &size, input) < 0) {
+            return ErrnoError() << "Failed to read";
+        }
+
+        std::string_view original = line;
+        if (!original.empty() && original.back() == '\n') {
+            original = original.substr(0, original.size() - 1);
+        }
+
+        std::string reversed(original.rbegin(), original.rend());
+        reversed += "\n";
+
+        if (write(connect_fd, reversed.data(), reversed.size()) < 0) {
+            return ErrnoError() << "Failed to write";
+        }
     }
-
-    if (fclose(input) != 0) {
-        return ErrnoError() << "Failed to fclose";
-    }
-
-    std::string_view original = line;
-    if (!original.empty() && original.back() == '\n') {
-        original = original.substr(0, original.size() - 1);
-    }
-
-    std::string reversed(original.rbegin(), original.rend());
-
-    if (write(connect_fd, reversed.data(), reversed.size()) < 0) {
-        return ErrnoError() << "Failed to write";
-    }
-
-    return {};
 }
 
 Result<void> start_echo_reverse_server() {
@@ -141,7 +148,25 @@
 }
 
 Result<void> start_test_service() {
+    class VmCallbackImpl : public BnVmCallback {
+    private:
+        std::shared_ptr<IAppCallback> mAppCallback;
+
+    public:
+        explicit VmCallbackImpl(const std::shared_ptr<IAppCallback>& appCallback)
+              : mAppCallback(appCallback) {}
+
+        ScopedAStatus echoMessage(const std::string& message) override {
+            std::thread callback_thread{[=, appCallback = mAppCallback] {
+                appCallback->onEchoRequestReceived("Received: " + message);
+            }};
+            callback_thread.detach();
+            return ScopedAStatus::ok();
+        }
+    };
+
     class TestService : public BnTestService {
+    public:
         ScopedAStatus addInteger(int32_t a, int32_t b, int32_t* out) override {
             *out = a + b;
             return ScopedAStatus::ok();
@@ -223,7 +248,7 @@
             return ScopedAStatus::ok();
         }
 
-        virtual ::ScopedAStatus runEchoReverseServer() override {
+        ScopedAStatus runEchoReverseServer() override {
             auto result = start_echo_reverse_server();
             if (result.ok()) {
                 return ScopedAStatus::ok();
@@ -253,6 +278,41 @@
             return ScopedAStatus::ok();
         }
 
+        ScopedAStatus getFilePermissions(const std::string& path, int32_t* out) override {
+            struct stat sb;
+            if (stat(path.c_str(), &sb) != -1) {
+                *out = sb.st_mode;
+            } else {
+                std::string msg = "stat " + path + " failed :  " + std::strerror(errno);
+                return ScopedAStatus::fromExceptionCodeWithMessage(EX_SERVICE_SPECIFIC,
+                                                                   msg.c_str());
+            }
+            return ScopedAStatus::ok();
+        }
+
+        ScopedAStatus getMountFlags(const std::string& mount_point, int32_t* out) override {
+            Fstab fstab;
+            if (!ReadFstabFromFile("/proc/mounts", &fstab)) {
+                return ScopedAStatus::fromExceptionCodeWithMessage(EX_SERVICE_SPECIFIC,
+                                                                   "Failed to read /proc/mounts");
+            }
+            FstabEntry* entry = GetEntryForMountPoint(&fstab, mount_point);
+            if (entry == nullptr) {
+                std::string msg = mount_point + " not found in /proc/mounts";
+                return ScopedAStatus::fromExceptionCodeWithMessage(EX_SERVICE_SPECIFIC,
+                                                                   msg.c_str());
+            }
+            *out = entry->flags;
+            return ScopedAStatus::ok();
+        }
+
+        ScopedAStatus requestCallback(const std::shared_ptr<IAppCallback>& appCallback) {
+            auto vmCallback = ndk::SharedRefBase::make<VmCallbackImpl>(appCallback);
+            std::thread callback_thread{[=] { appCallback->setVmCallback(vmCallback); }};
+            callback_thread.detach();
+            return ScopedAStatus::ok();
+        }
+
         ScopedAStatus quit() override { exit(0); }
     };
     auto testService = ndk::SharedRefBase::make<TestService>();
diff --git a/tests/vmshareapp/Android.bp b/tests/vmshareapp/Android.bp
new file mode 100644
index 0000000..6c2c9e4
--- /dev/null
+++ b/tests/vmshareapp/Android.bp
@@ -0,0 +1,16 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+// Helper app to verify that we can create a VM using others app payload, and share VMs between apps
+android_test_helper_app {
+    name: "MicrodroidVmShareApp",
+    srcs: ["src/java/**/*.java"],
+    // Defaults are defined in ../testapk/Android.bp
+    defaults: ["MicrodroidTestAppsDefaults"],
+    jni_libs: [
+        // Defined in ../testapk/Android.bp
+        "MicrodroidPayloadInOtherAppNativeLib",
+    ],
+    min_sdk_version: "UpsideDownCake",
+}
diff --git a/tests/vmshareapp/AndroidManifest.xml b/tests/vmshareapp/AndroidManifest.xml
new file mode 100644
index 0000000..b623f7f
--- /dev/null
+++ b/tests/vmshareapp/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+      package="com.android.microdroid.vmshare_app">
+    <uses-permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE" />
+
+    <uses-feature android:name="android.software.virtualization_framework"
+                  android:required="false" />
+
+    <application>
+        <service android:name="com.android.microdroid.test.sharevm.VmShareServiceImpl"
+                 android:exported="true">
+            <intent-filter>
+                <action android:name="com.android.microdroid.test.sharevm.VmShareService"/>
+            </intent-filter>
+        </service>
+    </application>
+
+</manifest>
diff --git a/tests/vmshareapp/aidl/Android.bp b/tests/vmshareapp/aidl/Android.bp
new file mode 100644
index 0000000..df4a4b4
--- /dev/null
+++ b/tests/vmshareapp/aidl/Android.bp
@@ -0,0 +1,15 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+// Unfortunatelly aidl_interface doesn't work well with .aidl files that depend on java-only
+// parcelables (e.g. Bundle, VirtualMachineDescriptor), hence this java_library.
+java_library {
+    name: "com.android.microdroid.test.vmshare_service-java",
+    srcs: ["com/**/*.aidl"],
+    sdk_version: "test_current",
+    static_libs: ["com.android.microdroid.testservice-java"],
+    aidl: {
+        include_dirs: ["packages/modules/Virtualization/tests/aidl/"],
+    },
+}
diff --git a/tests/vmshareapp/aidl/com/android/microdroid/test/vmshare/IVmShareTestService.aidl b/tests/vmshareapp/aidl/com/android/microdroid/test/vmshare/IVmShareTestService.aidl
new file mode 100644
index 0000000..fe6ca43
--- /dev/null
+++ b/tests/vmshareapp/aidl/com/android/microdroid/test/vmshare/IVmShareTestService.aidl
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2023 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.microdroid.test.vmshare;
+
+import android.system.virtualmachine.VirtualMachineDescriptor;
+import com.android.microdroid.testservice.ITestService;
+
+/** {@hide} */
+interface IVmShareTestService {
+    ITestService startVm(in VirtualMachineDescriptor vmDesc);
+}
diff --git a/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java b/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java
new file mode 100644
index 0000000..edd6bf5
--- /dev/null
+++ b/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2023 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.microdroid.test.sharevm;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.system.virtualmachine.VirtualMachine;
+import android.system.virtualmachine.VirtualMachineCallback;
+import android.system.virtualmachine.VirtualMachineDescriptor;
+import android.system.virtualmachine.VirtualMachineException;
+import android.system.virtualmachine.VirtualMachineManager;
+import android.util.Log;
+
+import com.android.microdroid.test.vmshare.IVmShareTestService;
+import com.android.microdroid.testservice.ITestService;
+import com.android.microdroid.testservice.IAppCallback;
+
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * A {@link Service} that is used in end-to-end tests of the {@link VirtualMachine} sharing
+ * functionality.
+ *
+ * <p>During the test {@link com.android.microdroid.test.MicrodroidTests} will bind to this service,
+ * and call {@link #startVm(VirtualMachineDescriptor)} to share the VM. This service then will
+ * create a {@link VirtualMachine} from that descriptor, {@link VirtualMachine#run() run} it, and
+ * send back {@link RemoteTestServiceDelegate}. The {@code MicrodroidTests} can use that {@link
+ * RemoteTestServiceDelegate} to assert conditions on the VM running in the {@link
+ * VmShareServiceImpl}.
+ *
+ * <p>The {@link VirtualMachine} running in this service will be stopped on {@link
+ * #onUnbind(Intent)}.
+ *
+ * @see com.android.microdroid.test.MicrodroidTests#testShareVmWithAnotherApp
+ */
+public class VmShareServiceImpl extends Service {
+
+    private static final String TAG = "VmShareApp";
+
+    private IVmShareTestService.Stub mBinder;
+
+    private VirtualMachine mVirtualMachine;
+
+    @Override
+    public void onCreate() {
+        mBinder = new ServiceImpl();
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        Log.i(TAG, "onBind " + intent + " binder = " + mBinder);
+        return mBinder;
+    }
+
+    @Override
+    public boolean onUnbind(Intent intent) {
+        deleteVm();
+        // Tell framework that it shouldn't call onRebind.
+        return false;
+    }
+
+    private void deleteVm() {
+        if (mVirtualMachine == null) {
+            return;
+        }
+        try {
+            mVirtualMachine.stop();
+            String name = mVirtualMachine.getName();
+            VirtualMachineManager vmm = getSystemService(VirtualMachineManager.class);
+            vmm.delete(name);
+            mVirtualMachine = null;
+        } catch (VirtualMachineException e) {
+            Log.e(TAG, "Failed to stop " + mVirtualMachine, e);
+        }
+    }
+
+    public ITestService startVm(VirtualMachineDescriptor vmDesc) throws Exception {
+        // Cleanup VM left from the previous test.
+        deleteVm();
+
+        VirtualMachineManager vmm = getSystemService(VirtualMachineManager.class);
+
+        // Add random uuid to make sure that different tests that bind to this service don't trip
+        // over each other.
+        String vmName = "imported_vm" + UUID.randomUUID();
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        VirtualMachineCallback callback =
+                new VirtualMachineCallback() {
+
+                    @Override
+                    public void onPayloadStarted(VirtualMachine vm) {
+                        // Ignored
+                    }
+
+                    @Override
+                    public void onPayloadReady(VirtualMachine vm) {
+                        latch.countDown();
+                    }
+
+                    @Override
+                    public void onPayloadFinished(VirtualMachine vm, int exitCode) {
+                        // Ignored
+                    }
+
+                    @Override
+                    public void onError(VirtualMachine vm, int errorCode, String message) {
+                        throw new RuntimeException(
+                                "VM failed with error " + errorCode + " : " + message);
+                    }
+
+                    @Override
+                    public void onStopped(VirtualMachine vm, int reason) {
+                        // Ignored
+                    }
+                };
+
+        mVirtualMachine = vmm.importFromDescriptor(vmName, vmDesc);
+        mVirtualMachine.setCallback(getMainExecutor(), callback);
+
+        Log.i(TAG, "Starting VM " + vmName);
+        mVirtualMachine.run();
+        if (!latch.await(1, TimeUnit.MINUTES)) {
+            throw new TimeoutException("Timed out starting VM");
+        }
+
+        Log.i(
+                TAG,
+                "Payload is ready, connecting to the vsock service at port "
+                        + ITestService.SERVICE_PORT);
+        ITestService testService =
+                ITestService.Stub.asInterface(
+                        mVirtualMachine.connectToVsockServer(ITestService.SERVICE_PORT));
+        return new RemoteTestServiceDelegate(testService);
+    }
+
+    final class ServiceImpl extends IVmShareTestService.Stub {
+
+        @Override
+        public ITestService startVm(VirtualMachineDescriptor vmDesc) {
+            Log.i(TAG, "startVm binder call received");
+            try {
+                return VmShareServiceImpl.this.startVm(vmDesc);
+            } catch (Exception e) {
+                Log.e(TAG, "Failed to startVm", e);
+                throw new IllegalStateException("Failed to startVm", e);
+            }
+        }
+    }
+
+    private static class RemoteTestServiceDelegate extends ITestService.Stub {
+
+        private final ITestService mServiceInVm;
+
+        private RemoteTestServiceDelegate(ITestService serviceInVm) {
+            mServiceInVm = serviceInVm;
+        }
+
+        @Override
+        public int addInteger(int a, int b) throws RemoteException {
+            return mServiceInVm.addInteger(a, b);
+        }
+
+        @Override
+        public String readProperty(String prop) throws RemoteException {
+            throw new UnsupportedOperationException("Not supported");
+        }
+
+        @Override
+        public byte[] insecurelyExposeVmInstanceSecret() throws RemoteException {
+            throw new UnsupportedOperationException("Not supported");
+        }
+
+        @Override
+        public byte[] insecurelyExposeAttestationCdi() throws RemoteException {
+            throw new UnsupportedOperationException("Not supported");
+        }
+
+        @Override
+        public byte[] getBcc() throws RemoteException {
+            throw new UnsupportedOperationException("Not supported");
+        }
+
+        @Override
+        public String getApkContentsPath() throws RemoteException {
+            throw new UnsupportedOperationException("Not supported");
+        }
+
+        @Override
+        public String getEncryptedStoragePath() throws RemoteException {
+            throw new UnsupportedOperationException("Not supported");
+        }
+
+        @Override
+        public void runEchoReverseServer() throws RemoteException {
+            throw new UnsupportedOperationException("Not supported");
+        }
+
+        @Override
+        public String[] getEffectiveCapabilities() throws RemoteException {
+            throw new UnsupportedOperationException("Not supported");
+        }
+
+        @Override
+        public void writeToFile(String content, String path) throws RemoteException {
+            throw new UnsupportedOperationException("Not supported");
+        }
+
+        @Override
+        public String readFromFile(String path) throws RemoteException {
+            return mServiceInVm.readFromFile(path);
+        }
+
+        @Override
+        public int getFilePermissions(String path) throws RemoteException {
+            throw new UnsupportedOperationException("Not supported");
+        }
+
+        @Override
+        public int getMountFlags(String path) throws RemoteException {
+            throw new UnsupportedOperationException("Not supported");
+        }
+
+        @Override
+        public void requestCallback(IAppCallback appCallback) {
+            throw new UnsupportedOperationException("Not supported");
+        }
+
+        @Override
+        public void quit() throws RemoteException {
+            throw new UnsupportedOperationException("Not supported");
+        }
+    }
+}
diff --git a/virtualizationmanager/Android.bp b/virtualizationmanager/Android.bp
index a436cea..c913d02 100644
--- a/virtualizationmanager/Android.bp
+++ b/virtualizationmanager/Android.bp
@@ -32,6 +32,7 @@
         "libclap",
         "libcommand_fds",
         "libdisk",
+        "libhypervisor_props",
         "liblazy_static",
         "liblibc",
         "liblog_rust",
@@ -59,7 +60,6 @@
         "packagemanager_aidl-rust",
     ],
     shared_libs: [
-        "libbinder_rpc_unstable",
         "libselinux",
     ],
 }
diff --git a/virtualizationmanager/src/aidl.rs b/virtualizationmanager/src/aidl.rs
index cab7c71..e015d9d 100644
--- a/virtualizationmanager/src/aidl.rs
+++ b/virtualizationmanager/src/aidl.rs
@@ -19,6 +19,8 @@
     write_vm_booted_stats, write_vm_creation_stats};
 use crate::composite::make_composite_image;
 use crate::crosvm::{CrosvmConfig, DiskFile, PayloadState, VmContext, VmInstance, VmState};
+use crate::debug_config::should_prepare_console_output;
+use crate::debug_config::is_ramdump_needed;
 use crate::payload::{add_microdroid_payload_images, add_microdroid_system_images};
 use crate::selinux::{getfilecon, SeContext};
 use android_os_permissions_aidl::aidl::android::os::IPermissionController;
@@ -27,6 +29,7 @@
     ErrorCode::ErrorCode,
 };
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
+    CpuTopology::CpuTopology,
     DiskImage::DiskImage,
     IVirtualMachine::{BnVirtualMachine, IVirtualMachine},
     IVirtualMachineCallback::IVirtualMachineCallback,
@@ -45,7 +48,7 @@
 use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice::IVirtualMachineService::{
         BnVirtualMachineService, IVirtualMachineService,
 };
-use anyhow::{bail, Context, Result};
+use anyhow::{anyhow, bail, Context, Result};
 use apkverify::{HashAlgorithm, V4Signature};
 use binder::{
     self, wait_for_interface, BinderFeatures, ExceptionCode, Interface, ParcelFileDescriptor,
@@ -57,12 +60,13 @@
 use microdroid_payload_config::{OsConfig, Task, TaskType, VmPayloadConfig};
 use nix::unistd::pipe;
 use rpcbinder::RpcServer;
+use rustutils::system_properties;
 use semver::VersionReq;
 use std::convert::TryInto;
 use std::ffi::CStr;
 use std::fs::{read_dir, remove_file, File, OpenOptions};
 use std::io::{BufRead, BufReader, Error, ErrorKind, Write};
-use std::num::NonZeroU32;
+use std::num::{NonZeroU16, NonZeroU32};
 use std::os::unix::io::{FromRawFd, IntoRawFd};
 use std::os::unix::raw::pid_t;
 use std::path::{Path, PathBuf};
@@ -108,8 +112,9 @@
     if !metadata.is_file() {
         bail!("input is not a regular file");
     }
-    let mut sig = V4Signature::create(&mut input, 4096, &[], HashAlgorithm::SHA256)
-        .context("failed to create idsig")?;
+    let mut sig =
+        V4Signature::create(&mut input, get_current_sdk()?, 4096, &[], HashAlgorithm::SHA256)
+            .context("failed to create idsig")?;
 
     let mut output = clone_file(idsig_fd)?;
     output.set_len(0).context("failed to set_len on the idsig output")?;
@@ -117,6 +122,12 @@
     Ok(())
 }
 
+fn get_current_sdk() -> Result<u32> {
+    let current_sdk = system_properties::read("ro.build.version.sdk")?;
+    let current_sdk = current_sdk.ok_or_else(|| anyhow!("SDK version missing"))?;
+    current_sdk.parse().context("Malformed SDK version")
+}
+
 pub fn remove_temporary_files(path: &PathBuf) -> Result<()> {
     for dir_entry in read_dir(path)? {
         remove_file(dir_entry?.path())?;
@@ -298,14 +309,35 @@
                 // Some features are reserved for platform apps only, even when using
                 // VirtualMachineAppConfig:
                 // - controlling CPUs;
-                // - specifying a config file in the APK.
-                !config.taskProfiles.is_empty() || matches!(config.payload, Payload::ConfigPath(_))
+                // - specifying a config file in the APK;
+                // - gdbPort is set, meaning that crosvm will start a gdb server.
+                !config.taskProfiles.is_empty()
+                    || matches!(config.payload, Payload::ConfigPath(_))
+                    || config.gdbPort > 0
             }
         };
         if is_custom {
             check_use_custom_virtual_machine()?;
         }
 
+        let gdb_port = extract_gdb_port(config);
+
+        // Additional permission checks if caller request gdb.
+        if gdb_port.is_some() {
+            check_gdb_allowed(config)?;
+        }
+
+        let ramdump = if is_ramdump_needed(config) {
+            Some(prepare_ramdump_file(&temporary_directory)?)
+        } else {
+            None
+        };
+
+        let debug_level = match config {
+            VirtualMachineConfig::AppConfig(app_config) => app_config.debugLevel,
+            _ => DebugLevel::NONE,
+        };
+
         let state = &mut *self.state.lock().unwrap();
         let console_fd =
             clone_or_prepare_logger_fd(config, console_fd, format!("Console({})", cid))?;
@@ -384,18 +416,17 @@
             })
             .collect::<Result<Vec<DiskFile>, _>>()?;
 
-        // Creating this ramdump file unconditionally is not harmful as ramdump will be created
-        // only when the VM is configured as such. `ramdump_write` is sent to crosvm and will
-        // be the backing store for the /dev/hvc1 where VM will emit ramdump to. `ramdump_read`
-        // will be sent back to the client (i.e. the VM owner) for readout.
-        let ramdump_path = temporary_directory.join("ramdump");
-        let ramdump = prepare_ramdump_file(&ramdump_path).map_err(|e| {
-            error!("Failed to prepare ramdump file: {:?}", e);
-            Status::new_service_specific_error_str(
-                -1,
-                Some(format!("Failed to prepare ramdump file: {:?}", e)),
-            )
-        })?;
+        let (cpus, host_cpu_topology) = match config.cpuTopology {
+            CpuTopology::MATCH_HOST => (None, true),
+            CpuTopology::ONE_CPU => (NonZeroU32::new(1), false),
+            val => {
+                error!("Unexpected value of CPU topology: {:?}", val);
+                return Err(Status::new_service_specific_error_str(
+                    -1,
+                    Some(format!("Failed to parse CPU topology value: {:?}", val)),
+                ));
+            }
+        };
 
         // Actually start the VM.
         let crosvm_config = CrosvmConfig {
@@ -407,15 +438,18 @@
             disks,
             params: config.params.to_owned(),
             protected: *is_protected,
+            debug_level,
             memory_mib: config.memoryMib.try_into().ok().and_then(NonZeroU32::new),
-            cpus: config.numCpus.try_into().ok().and_then(NonZeroU32::new),
+            cpus,
+            host_cpu_topology,
             task_profiles: config.taskProfiles.clone(),
             console_fd,
             log_fd,
-            ramdump: Some(ramdump),
+            ramdump,
             indirect_files,
             platform_version: parse_platform_version_req(&config.platformVersion)?,
             detect_hangup: is_app_config,
+            gdb_port,
         };
         let instance = Arc::new(
             VmInstance::new(
@@ -460,10 +494,6 @@
     part.flush()
 }
 
-fn prepare_ramdump_file(ramdump_path: &Path) -> Result<File> {
-    File::create(ramdump_path).context(format!("Failed to create ramdump file {:?}", &ramdump_path))
-}
-
 fn round_up(input: u64, granularity: u64) -> u64 {
     if granularity == 0 {
         return input;
@@ -567,8 +597,9 @@
 
     vm_config.name = config.name.clone();
     vm_config.protectedVm = config.protectedVm;
-    vm_config.numCpus = config.numCpus;
+    vm_config.cpuTopology = config.cpuTopology;
     vm_config.taskProfiles = config.taskProfiles.clone();
+    vm_config.gdbPort = config.gdbPort;
 
     // Microdroid takes additional init ramdisk & (optionally) storage image
     add_microdroid_system_images(config, instance_file, storage_image, &mut vm_config)?;
@@ -611,7 +642,7 @@
         apexes: vec![],
         extra_apks: vec![],
         prefer_staged: false,
-        export_tombstones: false,
+        export_tombstones: None,
         enable_authfs: false,
     })
 }
@@ -952,10 +983,56 @@
     })
 }
 
-fn is_debuggable(config: &VirtualMachineConfig) -> bool {
+/// Create the empty ramdump file
+fn prepare_ramdump_file(temporary_directory: &Path) -> binder::Result<File> {
+    // `ramdump_write` is sent to crosvm and will be the backing store for the /dev/hvc1 where
+    // VM will emit ramdump to. `ramdump_read` will be sent back to the client (i.e. the VM
+    // owner) for readout.
+    let ramdump_path = temporary_directory.join("ramdump");
+    let ramdump = File::create(ramdump_path).map_err(|e| {
+        error!("Failed to prepare ramdump file: {:?}", e);
+        Status::new_service_specific_error_str(
+            -1,
+            Some(format!("Failed to prepare ramdump file: {:?}", e)),
+        )
+    })?;
+    Ok(ramdump)
+}
+
+fn is_protected(config: &VirtualMachineConfig) -> bool {
     match config {
-        VirtualMachineConfig::AppConfig(config) => config.debugLevel != DebugLevel::NONE,
-        _ => false,
+        VirtualMachineConfig::RawConfig(config) => config.protectedVm,
+        VirtualMachineConfig::AppConfig(config) => config.protectedVm,
+    }
+}
+
+fn check_gdb_allowed(config: &VirtualMachineConfig) -> binder::Result<()> {
+    if is_protected(config) {
+        return Err(Status::new_exception_str(
+            ExceptionCode::SECURITY,
+            Some("can't use gdb with protected VMs"),
+        ));
+    }
+
+    match config {
+        VirtualMachineConfig::RawConfig(_) => Ok(()),
+        VirtualMachineConfig::AppConfig(config) => {
+            if config.debugLevel != DebugLevel::FULL {
+                Err(Status::new_exception_str(
+                    ExceptionCode::SECURITY,
+                    Some("can't use gdb with non-debuggable VMs"),
+                ))
+            } else {
+                Ok(())
+            }
+        }
+    }
+}
+
+fn extract_gdb_port(config: &VirtualMachineConfig) -> Option<NonZeroU16> {
+    match config {
+        VirtualMachineConfig::RawConfig(config) => NonZeroU16::new(config.gdbPort as u16),
+        VirtualMachineConfig::AppConfig(config) => NonZeroU16::new(config.gdbPort as u16),
     }
 }
 
@@ -968,9 +1045,12 @@
         return Ok(Some(clone_file(fd)?));
     }
 
-    if !is_debuggable(config) {
+    let VirtualMachineConfig::AppConfig(app_config) = config else {
         return Ok(None);
-    }
+    };
+    if !should_prepare_console_output(app_config.debugLevel) {
+        return Ok(None);
+    };
 
     let (raw_read_fd, raw_write_fd) = pipe().map_err(|e| {
         Status::new_service_specific_error_str(-1, Some(format!("Failed to create pipe: {:?}", e)))
diff --git a/virtualizationmanager/src/atom.rs b/virtualizationmanager/src/atom.rs
index c33f262..567fce9 100644
--- a/virtualizationmanager/src/atom.rs
+++ b/virtualizationmanager/src/atom.rs
@@ -19,6 +19,7 @@
 use crate::get_calling_uid;
 use android_system_virtualizationcommon::aidl::android::system::virtualizationcommon::DeathReason::DeathReason;
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
+    CpuTopology::CpuTopology,
     IVirtualMachine::IVirtualMachine,
     VirtualMachineAppConfig::{Payload::Payload, VirtualMachineAppConfig},
     VirtualMachineConfig::VirtualMachineConfig,
@@ -38,6 +39,8 @@
 use std::time::{Duration, SystemTime};
 use zip::ZipArchive;
 
+const INVALID_NUM_CPUS: i32 = -1;
+
 fn get_apex_list(config: &VirtualMachineAppConfig) -> String {
     match &config.payload {
         Payload::PayloadConfig(_) => String::new(),
@@ -76,6 +79,19 @@
     }
 }
 
+// Returns the number of CPUs configured in the host system.
+// This matches how crosvm determines the number of logical cores.
+// For telemetry purposes only.
+pub(crate) fn get_num_cpus() -> Option<usize> {
+    // SAFETY - Only integer constants passed back and forth.
+    let ret = unsafe { libc::sysconf(libc::_SC_NPROCESSORS_CONF) };
+    if ret > 0 {
+        ret.try_into().ok()
+    } else {
+        None
+    }
+}
+
 /// Write the stats of VMCreation to statsd
 pub fn write_vm_creation_stats(
     config: &VirtualMachineConfig,
@@ -94,23 +110,33 @@
             binder_exception_code = e.exception_code() as i32;
         }
     }
-    let (vm_identifier, config_type, num_cpus, memory_mib, apexes) = match config {
+    let (vm_identifier, config_type, cpu_topology, memory_mib, apexes) = match config {
         VirtualMachineConfig::AppConfig(config) => (
             config.name.clone(),
             vm_creation_requested::ConfigType::VirtualMachineAppConfig,
-            config.numCpus,
+            config.cpuTopology,
             config.memoryMib,
             get_apex_list(config),
         ),
         VirtualMachineConfig::RawConfig(config) => (
             config.name.clone(),
             vm_creation_requested::ConfigType::VirtualMachineRawConfig,
-            config.numCpus,
+            config.cpuTopology,
             config.memoryMib,
             String::new(),
         ),
     };
 
+    let num_cpus: i32 = match cpu_topology {
+        CpuTopology::MATCH_HOST => {
+            get_num_cpus().and_then(|v| v.try_into().ok()).unwrap_or_else(|| {
+                warn!("Failed to determine the number of CPUs in the host");
+                INVALID_NUM_CPUS
+            })
+        }
+        _ => 1,
+    };
+
     let atom = AtomVmCreationRequested {
         uid: get_calling_uid() as i32,
         vmIdentifier: vm_identifier,
diff --git a/virtualizationmanager/src/crosvm.rs b/virtualizationmanager/src/crosvm.rs
index 8f88daf..9db0971 100644
--- a/virtualizationmanager/src/crosvm.rs
+++ b/virtualizationmanager/src/crosvm.rs
@@ -15,7 +15,8 @@
 //! Functions for running instances of `crosvm`.
 
 use crate::aidl::{remove_temporary_files, Cid, VirtualMachineCallbacks};
-use crate::atom::write_vm_exited_stats;
+use crate::atom::{get_num_cpus, write_vm_exited_stats};
+use crate::debug_config::should_prepare_console_output;
 use anyhow::{anyhow, bail, Context, Error, Result};
 use command_fds::CommandFdExt;
 use lazy_static::lazy_static;
@@ -32,7 +33,7 @@
 use std::fs::{read_to_string, File};
 use std::io::{self, Read};
 use std::mem;
-use std::num::NonZeroU32;
+use std::num::{NonZeroU16, NonZeroU32};
 use std::os::unix::io::{AsRawFd, RawFd, FromRawFd};
 use std::os::unix::process::ExitStatusExt;
 use std::path::{Path, PathBuf};
@@ -41,7 +42,10 @@
 use std::time::{Duration, SystemTime};
 use std::thread::{self, JoinHandle};
 use android_system_virtualizationcommon::aidl::android::system::virtualizationcommon::DeathReason::DeathReason;
-use android_system_virtualizationservice::aidl::android::system::virtualizationservice::MemoryTrimLevel::MemoryTrimLevel;
+use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
+    MemoryTrimLevel::MemoryTrimLevel,
+    VirtualMachineAppConfig::DebugLevel::DebugLevel
+};
 use android_system_virtualizationservice_internal::aidl::android::system::virtualizationservice_internal::IGlobalVmContext::IGlobalVmContext;
 use binder::Strong;
 use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice::IVirtualMachineService::IVirtualMachineService;
@@ -68,6 +72,8 @@
 const CROSVM_CRASH_STATUS: i32 = 33;
 /// The exit status which crosvm returns when vcpu is stalled.
 const CROSVM_WATCHDOG_REBOOT_STATUS: i32 = 36;
+/// The size of memory (in MiB) reserved for ramdump
+const RAMDUMP_RESERVED_MIB: u32 = 17;
 
 const MILLIS_PER_SEC: i64 = 1000;
 
@@ -95,8 +101,10 @@
     pub disks: Vec<DiskFile>,
     pub params: Option<String>,
     pub protected: bool,
+    pub debug_level: DebugLevel,
     pub memory_mib: Option<NonZeroU32>,
     pub cpus: Option<NonZeroU32>,
+    pub host_cpu_topology: bool,
     pub task_profiles: Vec<String>,
     pub console_fd: Option<File>,
     pub log_fd: Option<File>,
@@ -104,6 +112,7 @@
     pub indirect_files: Vec<File>,
     pub platform_version: VersionReq,
     pub detect_hangup: bool,
+    pub gdb_port: Option<NonZeroU16>,
 }
 
 /// A disk image to pass to crosvm for a VM.
@@ -528,6 +537,9 @@
     /// Checks if ramdump has been created. If so, send it to tombstoned.
     fn handle_ramdump(&self) -> Result<(), Error> {
         let ramdump_path = self.temporary_directory.join("ramdump");
+        if !ramdump_path.as_path().try_exists()? {
+            return Ok(());
+        }
         if std::fs::metadata(&ramdump_path)?.len() > 0 {
             Self::send_ramdump_to_tombstoned(&ramdump_path)?;
         }
@@ -702,8 +714,32 @@
         let virtio_pci_device_count = 4 + config.disks.len();
         // crosvm virtio queue has 256 entries, so 2 MiB per device (2 pages per entry) should be
         // enough.
-        let swiotlb_size_mib = 2 * virtio_pci_device_count;
+        let swiotlb_size_mib = 2 * virtio_pci_device_count as u32;
         command.arg("--swiotlb").arg(swiotlb_size_mib.to_string());
+
+        // Workaround to keep crash_dump from trying to read protected guest memory.
+        // Context in b/238324526.
+        command.arg("--unmap-guest-memory-on-fork");
+
+        if config.ramdump.is_some() {
+            // Protected VM needs to reserve memory for ramdump here. pvmfw will drop This
+            // if ramdump should be disabled (via debug policy). Note that we reserve more
+            // memory for the restricted dma pool.
+            let ramdump_reserve = RAMDUMP_RESERVED_MIB + swiotlb_size_mib;
+            command.arg("--params").arg(format!("crashkernel={ramdump_reserve}M"));
+        }
+    } else {
+        if config.ramdump.is_some() {
+            command.arg("--params").arg(format!("crashkernel={RAMDUMP_RESERVED_MIB}M"));
+        }
+        if config.debug_level == DebugLevel::NONE
+            && should_prepare_console_output(config.debug_level)
+        {
+            // bootconfig.normal will be used, but we need log.
+            // pvmfw will add following commands by itself, but non-protected VM should do so here.
+            command.arg("--params").arg("printk.devkmsg=on");
+            command.arg("--params").arg("console=hvc0");
+        }
     }
 
     if let Some(memory_mib) = config.memory_mib {
@@ -714,10 +750,23 @@
         command.arg("--cpus").arg(cpus.to_string());
     }
 
+    if config.host_cpu_topology {
+        // TODO(b/266664564): replace with --host-cpu-topology once available
+        if let Some(cpus) = get_num_cpus() {
+            command.arg("--cpus").arg(cpus.to_string());
+        } else {
+            bail!("Could not determine the number of CPUs in the system");
+        }
+    }
+
     if !config.task_profiles.is_empty() {
         command.arg("--task-profiles").arg(config.task_profiles.join(","));
     }
 
+    if let Some(gdb_port) = config.gdb_port {
+        command.arg("--gdb").arg(gdb_port.to_string());
+    }
+
     // 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();
 
@@ -779,7 +828,6 @@
     debug!("Preserving FDs {:?}", preserved_fds);
     command.preserved_fds(preserved_fds);
 
-    command.arg("--params").arg("crashkernel=17M");
     print_crosvm_args(&command);
 
     let result = SharedChild::spawn(&mut command)?;
diff --git a/virtualizationmanager/src/debug_config.rs b/virtualizationmanager/src/debug_config.rs
new file mode 100644
index 0000000..666c98d
--- /dev/null
+++ b/virtualizationmanager/src/debug_config.rs
@@ -0,0 +1,59 @@
+// Copyright 2023, 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 AVF debug policy and debug level
+
+use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
+    VirtualMachineAppConfig::DebugLevel::DebugLevel, VirtualMachineConfig::VirtualMachineConfig,
+};
+use std::fs::File;
+use std::io::Read;
+
+/// Get debug policy value in bool. It's true iff the value is explicitly set to <1>.
+fn get_debug_policy_bool(path: &'static str) -> Option<bool> {
+    let mut file = File::open(path).ok()?;
+    let mut log: [u8; 4] = Default::default();
+    file.read_exact(&mut log).ok()?;
+    // DT spec uses big endian although Android is always little endian.
+    Some(u32::from_be_bytes(log) == 1)
+}
+
+/// Get whether console output should be configred for VM to leave console and adb log.
+/// Caller should create pipe and prepare for receiving VM log with it.
+pub fn should_prepare_console_output(debug_level: DebugLevel) -> bool {
+    debug_level != DebugLevel::NONE
+        || get_debug_policy_bool("/proc/device-tree/avf/guest/common/log").unwrap_or_default()
+        || get_debug_policy_bool("/proc/device-tree/avf/guest/microdroid/adb").unwrap_or_default()
+}
+
+/// Get whether debug apexes (MICRODROID_REQUIRED_APEXES_DEBUG) are required.
+pub fn should_include_debug_apexes(debug_level: DebugLevel) -> bool {
+    debug_level != DebugLevel::NONE
+        || get_debug_policy_bool("/proc/device-tree/avf/guest/microdroid/adb").unwrap_or_default()
+}
+
+/// Decision to support ramdump
+pub fn is_ramdump_needed(config: &VirtualMachineConfig) -> bool {
+    let enabled_in_dp =
+        get_debug_policy_bool("/proc/device-tree/avf/guest/common/ramdump").unwrap_or_default();
+    let debuggable = match config {
+        VirtualMachineConfig::RawConfig(_) => {
+            // custom VMs are considered debuggable for flexibility
+            true
+        }
+        VirtualMachineConfig::AppConfig(config) => config.debugLevel == DebugLevel::FULL,
+    };
+
+    enabled_in_dp || debuggable
+}
diff --git a/virtualizationmanager/src/main.rs b/virtualizationmanager/src/main.rs
index dca64cb..bd7f8af 100644
--- a/virtualizationmanager/src/main.rs
+++ b/virtualizationmanager/src/main.rs
@@ -18,12 +18,13 @@
 mod atom;
 mod composite;
 mod crosvm;
+mod debug_config;
 mod payload;
 mod selinux;
 
 use crate::aidl::{GLOBAL_SERVICE, VirtualizationService};
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::IVirtualizationService::BnVirtualizationService;
-use anyhow::{bail, Context};
+use anyhow::{bail, Context, Result};
 use binder::{BinderFeatures, ProcessState};
 use lazy_static::lazy_static;
 use log::{info, Level};
@@ -33,7 +34,6 @@
 use nix::fcntl::{fcntl, F_GETFD, F_SETFD, FdFlag};
 use nix::unistd::{Pid, Uid};
 use std::os::unix::raw::{pid_t, uid_t};
-use rustutils::system_properties;
 
 const LOG_TAG: &str = "virtmgr";
 
@@ -92,9 +92,15 @@
     Ok(unsafe { OwnedFd::from_raw_fd(raw_fd) })
 }
 
-fn is_property_set(name: &str) -> bool {
-    system_properties::read_bool(name, false)
-        .unwrap_or_else(|e| panic!("Failed to read {name}: {e:?}"))
+fn check_vm_support() -> Result<()> {
+    if hypervisor_props::is_any_vm_supported()? {
+        Ok(())
+    } else {
+        // This should never happen, it indicates a misconfigured device where the virt APEX
+        // is present but VMs are not supported. If it does happen, fail fast to avoid wasting
+        // resources trying.
+        bail!("Device doesn't support protected or non-protected VMs")
+    }
 }
 
 fn main() {
@@ -105,14 +111,7 @@
             .with_log_id(android_logger::LogId::System),
     );
 
-    let non_protected_vm_supported = is_property_set("ro.boot.hypervisor.vm.supported");
-    let protected_vm_supported = is_property_set("ro.boot.hypervisor.protected_vm.supported");
-    if !non_protected_vm_supported && !protected_vm_supported {
-        // This should never happen, it indicates a misconfigured device where the virt APEX
-        // is present but VMs are not supported. If it does happen, fail fast to avoid wasting
-        // resources trying.
-        panic!("Device doesn't support protected or unprotected VMs");
-    }
+    check_vm_support().unwrap();
 
     let args = Args::parse();
 
diff --git a/virtualizationmanager/src/payload.rs b/virtualizationmanager/src/payload.rs
index 02e8f8e..99aea01 100644
--- a/virtualizationmanager/src/payload.rs
+++ b/virtualizationmanager/src/payload.rs
@@ -14,6 +14,7 @@
 
 //! Payload disk image
 
+use crate::debug_config::should_include_debug_apexes;
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
     DiskImage::DiskImage,
     Partition::Partition,
@@ -382,7 +383,7 @@
     debug_level: DebugLevel,
 ) -> Vec<&'a ApexInfo> {
     let mut additional_apexes: Vec<&str> = MICRODROID_REQUIRED_APEXES.to_vec();
-    if debug_level != DebugLevel::NONE {
+    if should_include_debug_apexes(debug_level) {
         additional_apexes.extend(MICRODROID_REQUIRED_APEXES_DEBUG.to_vec());
     }
 
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/CpuTopology.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/CpuTopology.aidl
new file mode 100644
index 0000000..8a8e3d0
--- /dev/null
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/CpuTopology.aidl
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2023 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.system.virtualizationservice;
+
+/** The vCPU topology that will be generated for the VM. */
+@Backing(type="byte")
+enum CpuTopology {
+    /** One vCPU */
+    ONE_CPU = 0,
+    /** Match physical CPU topology of the host. */
+    MATCH_HOST = 1,
+}
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
index 884561d..c467c2f 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
@@ -15,6 +15,7 @@
  */
 package android.system.virtualizationservice;
 
+import android.system.virtualizationservice.CpuTopology;
 import android.system.virtualizationservice.VirtualMachinePayloadConfig;
 
 /** Configuration for running an App in a VM */
@@ -69,6 +70,12 @@
     /** Debug level of the VM */
     DebugLevel debugLevel = DebugLevel.NONE;
 
+    /**
+     * Port at which crosvm will start a gdb server to debug guest kernel.
+     * If set to zero, then gdb server won't be started.
+     */
+    int gdbPort = 0;
+
     /** Whether the VM should be a protected VM. */
     boolean protectedVm;
 
@@ -78,10 +85,8 @@
      */
     int memoryMib;
 
-    /**
-     * Number of vCPUs in the VM. Defaults to 1.
-     */
-    int numCpus = 1;
+    /** The vCPU topology that will be generated for the VM. Default to 1 vCPU. */
+    CpuTopology cpuTopology = CpuTopology.ONE_CPU;
 
     /**
      * List of task profile names to apply for the VM
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
index 993bbb0..87d4ba2 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
@@ -15,6 +15,7 @@
  */
 package android.system.virtualizationservice;
 
+import android.system.virtualizationservice.CpuTopology;
 import android.system.virtualizationservice.DiskImage;
 
 /** Raw configuration for running a VM. */
@@ -49,10 +50,8 @@
     /** The amount of RAM to give the VM, in MiB. 0 or negative to use the default. */
     int memoryMib;
 
-    /**
-     * Number of vCPUs in the VM. Defaults to 1.
-     */
-    int numCpus = 1;
+    /** The vCPU topology that will be generated for the VM. Default to 1 vCPU. */
+    CpuTopology cpuTopology = CpuTopology.ONE_CPU;
 
     /**
      * A version or range of versions of the virtual platform that this config is compatible with.
@@ -64,4 +63,10 @@
      * List of task profile names to apply for the VM
      */
     String[] taskProfiles;
+
+    /**
+     * Port at which crosvm will start a gdb server to debug guest kernel.
+     * If set to zero, then gdb server won't be started.
+     */
+    int gdbPort = 0;
 }
diff --git a/vm/Android.bp b/vm/Android.bp
index e217786..50e68cc 100644
--- a/vm/Android.bp
+++ b/vm/Android.bp
@@ -15,11 +15,11 @@
         "libclap",
         "libenv_logger",
         "libglob",
+        "libhypervisor_props",
         "liblibc",
         "liblog_rust",
         "libmicrodroid_payload_config",
         "librand",
-        "librustutils",
         "libserde_json",
         "libserde",
         "libvmconfig",
diff --git a/vm/src/main.rs b/vm/src/main.rs
index ea744f7..1d9f50b 100644
--- a/vm/src/main.rs
+++ b/vm/src/main.rs
@@ -19,16 +19,16 @@
 mod run;
 
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
-    IVirtualizationService::IVirtualizationService, PartitionType::PartitionType,
-    VirtualMachineAppConfig::DebugLevel::DebugLevel,
+    CpuTopology::CpuTopology, IVirtualizationService::IVirtualizationService,
+    PartitionType::PartitionType, VirtualMachineAppConfig::DebugLevel::DebugLevel,
 };
 use anyhow::{Context, Error};
-use binder::ProcessState;
+use binder::{ProcessState, Strong};
 use clap::Parser;
 use create_idsig::command_create_idsig;
 use create_partition::command_create_partition;
 use run::{command_run, command_run_app, command_run_microdroid};
-use rustutils::system_properties;
+use std::num::NonZeroU16;
 use std::path::{Path, PathBuf};
 
 #[derive(Debug)]
@@ -91,9 +91,9 @@
         #[clap(short, long)]
         mem: Option<u32>,
 
-        /// Number of vCPUs in the VM. If unspecified, defaults to 1.
-        #[clap(long)]
-        cpus: Option<u32>,
+        /// Run VM with vCPU topology matching that of the host. If unspecified, defaults to 1 vCPU.
+        #[clap(long, default_value = "one_cpu", value_parser = parse_cpu_topology)]
+        cpu_topology: CpuTopology,
 
         /// Comma separated list of task profile names to apply to the VM
         #[clap(long)]
@@ -102,6 +102,11 @@
         /// Paths to extra idsig files.
         #[clap(long = "extra-idsig")]
         extra_idsigs: Vec<PathBuf>,
+
+        /// Port at which crosvm will start a gdb server to debug guest kernel.
+        /// Note: this is only supported on Android kernels android14-5.15 and higher.
+        #[clap(long)]
+        gdb: Option<NonZeroU16>,
     },
     /// Run a virtual machine with Microdroid inside
     RunMicrodroid {
@@ -146,13 +151,18 @@
         #[clap(short, long)]
         mem: Option<u32>,
 
-        /// Number of vCPUs in the VM. If unspecified, defaults to 1.
-        #[clap(long)]
-        cpus: Option<u32>,
+        /// Run VM with vCPU topology matching that of the host. If unspecified, defaults to 1 vCPU.
+        #[clap(long, default_value = "one_cpu", value_parser = parse_cpu_topology)]
+        cpu_topology: CpuTopology,
 
         /// Comma separated list of task profile names to apply to the VM
         #[clap(long)]
         task_profiles: Vec<String>,
+
+        /// Port at which crosvm will start a gdb server to debug guest kernel.
+        /// Note: this is only supported on Android kernels android14-5.15 and higher.
+        #[clap(long)]
+        gdb: Option<NonZeroU16>,
     },
     /// Run a virtual machine
     Run {
@@ -163,9 +173,9 @@
         #[clap(long)]
         name: Option<String>,
 
-        /// Number of vCPUs in the VM. If unspecified, defaults to 1.
-        #[clap(long)]
-        cpus: Option<u32>,
+        /// Run VM with vCPU topology matching that of the host. If unspecified, defaults to 1 vCPU.
+        #[clap(long, default_value = "one_cpu", value_parser = parse_cpu_topology)]
+        cpu_topology: CpuTopology,
 
         /// Comma separated list of task profile names to apply to the VM
         #[clap(long)]
@@ -178,6 +188,11 @@
         /// Path to file for VM log output.
         #[clap(long)]
         log: Option<PathBuf>,
+
+        /// Port at which crosvm will start a gdb server to debug guest kernel.
+        /// Note: this is only supported on Android kernels android14-5.15 and higher.
+        #[clap(long)]
+        gdb: Option<NonZeroU16>,
     },
     /// List running virtual machines
     List,
@@ -222,6 +237,20 @@
     }
 }
 
+fn parse_cpu_topology(s: &str) -> Result<CpuTopology, String> {
+    match s {
+        "one_cpu" => Ok(CpuTopology::ONE_CPU),
+        "match_host" => Ok(CpuTopology::MATCH_HOST),
+        _ => Err(format!("Invalid cpu topology {}", s)),
+    }
+}
+
+fn get_service() -> Result<Strong<dyn IVirtualizationService>, Error> {
+    let virtmgr =
+        vmclient::VirtualizationService::new().context("Failed to spawn VirtualizationService")?;
+    virtmgr.connect().context("Failed to connect to VirtualizationService")
+}
+
 fn main() -> Result<(), Error> {
     env_logger::init();
     let opt = Opt::parse();
@@ -229,10 +258,6 @@
     // We need to start the thread pool for Binder to work properly, especially link_to_death.
     ProcessState::start_thread_pool();
 
-    let virtmgr =
-        vmclient::VirtualizationService::new().context("Failed to spawn VirtualizationService")?;
-    let service = virtmgr.connect().context("Failed to connect to VirtualizationService")?;
-
     match opt {
         Opt::RunApp {
             name,
@@ -248,12 +273,13 @@
             debug,
             protected,
             mem,
-            cpus,
+            cpu_topology,
             task_profiles,
             extra_idsigs,
+            gdb,
         } => command_run_app(
             name,
-            service.as_ref(),
+            get_service()?.as_ref(),
             &apk,
             &idsig,
             &instance,
@@ -266,9 +292,10 @@
             debug,
             protected,
             mem,
-            cpus,
+            cpu_topology,
             task_profiles,
             &extra_idsigs,
+            gdb,
         ),
         Opt::RunMicrodroid {
             name,
@@ -280,11 +307,12 @@
             debug,
             protected,
             mem,
-            cpus,
+            cpu_topology,
             task_profiles,
+            gdb,
         } => command_run_microdroid(
             name,
-            service.as_ref(),
+            get_service()?.as_ref(),
             work_dir,
             storage.as_deref(),
             storage_size,
@@ -293,27 +321,31 @@
             debug,
             protected,
             mem,
-            cpus,
+            cpu_topology,
             task_profiles,
+            gdb,
         ),
-        Opt::Run { name, config, cpus, task_profiles, console, log } => {
+        Opt::Run { name, config, cpu_topology, task_profiles, console, log, gdb } => {
             command_run(
                 name,
-                service.as_ref(),
+                get_service()?.as_ref(),
                 &config,
                 console.as_deref(),
                 log.as_deref(),
                 /* mem */ None,
-                cpus,
+                cpu_topology,
                 task_profiles,
+                gdb,
             )
         }
-        Opt::List => command_list(service.as_ref()),
+        Opt::List => command_list(get_service()?.as_ref()),
         Opt::Info => command_info(),
         Opt::CreatePartition { path, size, partition_type } => {
-            command_create_partition(service.as_ref(), &path, size, partition_type)
+            command_create_partition(get_service()?.as_ref(), &path, size, partition_type)
         }
-        Opt::CreateIdsig { apk, path } => command_create_idsig(service.as_ref(), &apk, &path),
+        Opt::CreateIdsig { apk, path } => {
+            command_create_idsig(get_service()?.as_ref(), &apk, &path)
+        }
     }
 }
 
@@ -326,10 +358,8 @@
 
 /// Print information about supported VM types.
 fn command_info() -> Result<(), Error> {
-    let non_protected_vm_supported =
-        system_properties::read_bool("ro.boot.hypervisor.vm.supported", false)?;
-    let protected_vm_supported =
-        system_properties::read_bool("ro.boot.hypervisor.protected_vm.supported", false)?;
+    let non_protected_vm_supported = hypervisor_props::is_vm_supported()?;
+    let protected_vm_supported = hypervisor_props::is_protected_vm_supported()?;
     match (non_protected_vm_supported, protected_vm_supported) {
         (false, false) => println!("VMs are not supported."),
         (false, true) => println!("Only protected VMs are supported."),
@@ -337,7 +367,7 @@
         (true, true) => println!("Both protected and non-protected VMs are supported."),
     }
 
-    if let Some(version) = system_properties::read("ro.boot.hypervisor.version")? {
+    if let Some(version) = hypervisor_props::version()? {
         println!("Hypervisor version: {}", version);
     } else {
         println!("Hypervisor version not set.");
diff --git a/vm/src/run.rs b/vm/src/run.rs
index e229933..36edc64 100644
--- a/vm/src/run.rs
+++ b/vm/src/run.rs
@@ -16,6 +16,7 @@
 
 use crate::create_partition::command_create_partition;
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
+    CpuTopology::CpuTopology,
     IVirtualizationService::IVirtualizationService,
     PartitionType::PartitionType,
     VirtualMachineAppConfig::{DebugLevel::DebugLevel, Payload::Payload, VirtualMachineAppConfig},
@@ -31,6 +32,7 @@
 use std::fs;
 use std::fs::File;
 use std::io;
+use std::num::NonZeroU16;
 use std::os::unix::io::{AsRawFd, FromRawFd};
 use std::path::{Path, PathBuf};
 use vmclient::{ErrorCode, VmInstance};
@@ -54,9 +56,10 @@
     debug_level: DebugLevel,
     protected: bool,
     mem: Option<u32>,
-    cpus: Option<u32>,
+    cpu_topology: CpuTopology,
     task_profiles: Vec<String>,
     extra_idsigs: &[PathBuf],
+    gdb: Option<NonZeroU16>,
 ) -> Result<(), Error> {
     let apk_file = File::open(apk).context("Failed to open APK file")?;
 
@@ -141,14 +144,15 @@
         debugLevel: debug_level,
         protectedVm: protected,
         memoryMib: mem.unwrap_or(0) as i32, // 0 means use the VM default
-        numCpus: cpus.unwrap_or(1) as i32,
+        cpuTopology: cpu_topology,
         taskProfiles: task_profiles,
+        gdbPort: gdb.map(u16::from).unwrap_or(0) as i32, // 0 means no gdb
     });
     run(service, &config, &payload_config_str, console_path, log_path)
 }
 
 fn find_empty_payload_apk_path() -> Result<PathBuf, Error> {
-    const GLOB_PATTERN: &str = "/apex/com.android.virt/app/**/EmptyPayloadApp.apk";
+    const GLOB_PATTERN: &str = "/apex/com.android.virt/app/**/EmptyPayloadApp*.apk";
     let mut entries: Vec<PathBuf> =
         glob(GLOB_PATTERN).context("failed to glob")?.filter_map(|e| e.ok()).collect();
     if entries.len() > 1 {
@@ -182,8 +186,9 @@
     debug_level: DebugLevel,
     protected: bool,
     mem: Option<u32>,
-    cpus: Option<u32>,
+    cpu_topology: CpuTopology,
     task_profiles: Vec<String>,
+    gdb: Option<NonZeroU16>,
 ) -> Result<(), Error> {
     let apk = find_empty_payload_apk_path()?;
     println!("found path {}", apk.display());
@@ -211,9 +216,10 @@
         debug_level,
         protected,
         mem,
-        cpus,
+        cpu_topology,
         task_profiles,
         &extra_sig,
+        gdb,
     )
 }
 
@@ -226,8 +232,9 @@
     console_path: Option<&Path>,
     log_path: Option<&Path>,
     mem: Option<u32>,
-    cpus: Option<u32>,
+    cpu_topology: CpuTopology,
     task_profiles: Vec<String>,
+    gdb: Option<NonZeroU16>,
 ) -> Result<(), Error> {
     let config_file = File::open(config_path).context("Failed to open config file")?;
     let mut config =
@@ -235,14 +242,15 @@
     if let Some(mem) = mem {
         config.memoryMib = mem as i32;
     }
-    if let Some(cpus) = cpus {
-        config.numCpus = cpus as i32;
-    }
     if let Some(name) = name {
         config.name = name;
     } else {
         config.name = String::from("VmRun");
     }
+    if let Some(gdb) = gdb {
+        config.gdbPort = gdb.get() as i32;
+    }
+    config.cpuTopology = cpu_topology;
     config.taskProfiles = task_profiles;
     run(
         service,
diff --git a/vmbase/entry.S b/vmbase/entry.S
index ab46465..408f5d1 100644
--- a/vmbase/entry.S
+++ b/vmbase/entry.S
@@ -225,6 +225,9 @@
 	adr_l x30, __stack_chk_guard
 	str x29, [x30]
 
+	/* Write a null byte to the top of the stack guard to act as a string terminator. */
+	strb wzr, [x30]
+
 	/* Call into Rust code. */
 	bl rust_entry
 
diff --git a/vmbase/example/Android.bp b/vmbase/example/Android.bp
index 94eb21a..26be51b 100644
--- a/vmbase/example/Android.bp
+++ b/vmbase/example/Android.bp
@@ -11,7 +11,7 @@
     rustlibs: [
         "libaarch64_paging",
         "libbuddy_system_allocator",
-        "libdice_nostd",
+        "libdiced_open_dice_nostd",
         "libfdtpci",
         "liblibfdt",
         "liblog_rust_nostd",
diff --git a/vmbase/example/src/main.rs b/vmbase/example/src/main.rs
index ec28a11..3b0e9db 100644
--- a/vmbase/example/src/main.rs
+++ b/vmbase/example/src/main.rs
@@ -225,7 +225,7 @@
 
 fn check_dice() {
     info!("Testing DICE integration...");
-    let hash = dice::hash("hello world".as_bytes()).expect("DiceHash failed");
+    let hash = diced_open_dice::hash("hello world".as_bytes()).expect("DiceHash failed");
     assert_eq!(
         hash,
         [
diff --git a/vmbase/example/tests/test.rs b/vmbase/example/tests/test.rs
index c6aea8c..930e137 100644
--- a/vmbase/example/tests/test.rs
+++ b/vmbase/example/tests/test.rs
@@ -16,7 +16,7 @@
 
 use android_system_virtualizationservice::{
     aidl::android::system::virtualizationservice::{
-        DiskImage::DiskImage, VirtualMachineConfig::VirtualMachineConfig,
+        CpuTopology::CpuTopology, DiskImage::DiskImage, VirtualMachineConfig::VirtualMachineConfig,
         VirtualMachineRawConfig::VirtualMachineRawConfig,
     },
     binder::{ParcelFileDescriptor, ProcessState},
@@ -84,9 +84,10 @@
         disks: vec![disk_image],
         protectedVm: false,
         memoryMib: 300,
-        numCpus: 1,
+        cpuTopology: CpuTopology::ONE_CPU,
         platformVersion: "~1.0".to_string(),
         taskProfiles: vec![],
+        gdbPort: 0, // no gdb
     });
     let console = android_log_fd()?;
     let log = android_log_fd()?;
diff --git a/vmbase/src/bionic.rs b/vmbase/src/bionic.rs
index 6f88cf6..69da521 100644
--- a/vmbase/src/bionic.rs
+++ b/vmbase/src/bionic.rs
@@ -126,3 +126,145 @@
         0
     }
 }
+
+#[no_mangle]
+extern "C" fn strerror(n: c_int) -> *mut c_char {
+    // Messages taken from errno(1).
+    let s = match n {
+        0 => "Success",
+        1 => "Operation not permitted",
+        2 => "No such file or directory",
+        3 => "No such process",
+        4 => "Interrupted system call",
+        5 => "Input/output error",
+        6 => "No such device or address",
+        7 => "Argument list too long",
+        8 => "Exec format error",
+        9 => "Bad file descriptor",
+        10 => "No child processes",
+        11 => "Resource temporarily unavailable",
+        12 => "Cannot allocate memory",
+        13 => "Permission denied",
+        14 => "Bad address",
+        15 => "Block device required",
+        16 => "Device or resource busy",
+        17 => "File exists",
+        18 => "Invalid cross-device link",
+        19 => "No such device",
+        20 => "Not a directory",
+        21 => "Is a directory",
+        22 => "Invalid argument",
+        23 => "Too many open files in system",
+        24 => "Too many open files",
+        25 => "Inappropriate ioctl for device",
+        26 => "Text file busy",
+        27 => "File too large",
+        28 => "No space left on device",
+        29 => "Illegal seek",
+        30 => "Read-only file system",
+        31 => "Too many links",
+        32 => "Broken pipe",
+        33 => "Numerical argument out of domain",
+        34 => "Numerical result out of range",
+        35 => "Resource deadlock avoided",
+        36 => "File name too long",
+        37 => "No locks available",
+        38 => "Function not implemented",
+        39 => "Directory not empty",
+        40 => "Too many levels of symbolic links",
+        42 => "No message of desired type",
+        43 => "Identifier removed",
+        44 => "Channel number out of range",
+        45 => "Level 2 not synchronized",
+        46 => "Level 3 halted",
+        47 => "Level 3 reset",
+        48 => "Link number out of range",
+        49 => "Protocol driver not attached",
+        50 => "No CSI structure available",
+        51 => "Level 2 halted",
+        52 => "Invalid exchange",
+        53 => "Invalid request descriptor",
+        54 => "Exchange full",
+        55 => "No anode",
+        56 => "Invalid request code",
+        57 => "Invalid slot",
+        59 => "Bad font file format",
+        60 => "Device not a stream",
+        61 => "No data available",
+        62 => "Timer expired",
+        63 => "Out of streams resources",
+        64 => "Machine is not on the network",
+        65 => "Package not installed",
+        66 => "Object is remote",
+        67 => "Link has been severed",
+        68 => "Advertise error",
+        69 => "Srmount error",
+        70 => "Communication error on send",
+        71 => "Protocol error",
+        72 => "Multihop attempted",
+        73 => "RFS specific error",
+        74 => "Bad message",
+        75 => "Value too large for defined data type",
+        76 => "Name not unique on network",
+        77 => "File descriptor in bad state",
+        78 => "Remote address changed",
+        79 => "Can not access a needed shared library",
+        80 => "Accessing a corrupted shared library",
+        81 => ".lib section in a.out corrupted",
+        82 => "Attempting to link in too many shared libraries",
+        83 => "Cannot exec a shared library directly",
+        84 => "Invalid or incomplete multibyte or wide character",
+        85 => "Interrupted system call should be restarted",
+        86 => "Streams pipe error",
+        87 => "Too many users",
+        88 => "Socket operation on non-socket",
+        89 => "Destination address required",
+        90 => "Message too long",
+        91 => "Protocol wrong type for socket",
+        92 => "Protocol not available",
+        93 => "Protocol not supported",
+        94 => "Socket type not supported",
+        95 => "Operation not supported",
+        96 => "Protocol family not supported",
+        97 => "Address family not supported by protocol",
+        98 => "Address already in use",
+        99 => "Cannot assign requested address",
+        100 => "Network is down",
+        101 => "Network is unreachable",
+        102 => "Network dropped connection on reset",
+        103 => "Software caused connection abort",
+        104 => "Connection reset by peer",
+        105 => "No buffer space available",
+        106 => "Transport endpoint is already connected",
+        107 => "Transport endpoint is not connected",
+        108 => "Cannot send after transport endpoint shutdown",
+        109 => "Too many references: cannot splice",
+        110 => "Connection timed out",
+        111 => "Connection refused",
+        112 => "Host is down",
+        113 => "No route to host",
+        114 => "Operation already in progress",
+        115 => "Operation now in progress",
+        116 => "Stale file handle",
+        117 => "Structure needs cleaning",
+        118 => "Not a XENIX named type file",
+        119 => "No XENIX semaphores available",
+        120 => "Is a named type file",
+        121 => "Remote I/O error",
+        122 => "Disk quota exceeded",
+        123 => "No medium found",
+        124 => "Wrong medium type",
+        125 => "Operation canceled",
+        126 => "Required key not available",
+        127 => "Key has expired",
+        128 => "Key has been revoked",
+        129 => "Key was rejected by service",
+        130 => "Owner died",
+        131 => "State not recoverable",
+        132 => "Operation not possible due to RF-kill",
+        133 => "Memory page has hardware error",
+        _ => "Unknown errno value",
+    };
+
+    s.as_ptr().cast_mut().cast()
+}
diff --git a/vmclient/Android.bp b/vmclient/Android.bp
index 0a2e692..8517c88 100644
--- a/vmclient/Android.bp
+++ b/vmclient/Android.bp
@@ -18,9 +18,6 @@
         "libshared_child",
         "libthiserror",
     ],
-    shared_libs: [
-        "libbinder_rpc_unstable",
-    ],
     apex_available: [
         "com.android.compos",
         "com.android.virt",
diff --git a/vmclient/src/lib.rs b/vmclient/src/lib.rs
index 0e3d140..d67d87e 100644
--- a/vmclient/src/lib.rs
+++ b/vmclient/src/lib.rs
@@ -57,7 +57,7 @@
     "android.system.virtualizationservice";
 
 const VIRTMGR_PATH: &str = "/apex/com.android.virt/bin/virtmgr";
-const VIRTMGR_THREADS: usize = 16;
+const VIRTMGR_THREADS: usize = 2;
 
 fn posix_pipe() -> Result<(OwnedFd, OwnedFd), io::Error> {
     use nix::fcntl::OFlag;
@@ -122,7 +122,6 @@
         let session = RpcSession::new();
         session.set_file_descriptor_transport_mode(FileDescriptorTransportMode::Unix);
         session.set_max_incoming_threads(VIRTMGR_THREADS);
-        session.set_max_outgoing_threads(VIRTMGR_THREADS);
         session
             .setup_unix_domain_bootstrap_client(self.client_fd.as_fd())
             .map_err(|_| io::Error::from(io::ErrorKind::ConnectionRefused))
diff --git a/zipfuse/src/inode.rs b/zipfuse/src/inode.rs
index 3edbc49..ea63422 100644
--- a/zipfuse/src/inode.rs
+++ b/zipfuse/src/inode.rs
@@ -99,12 +99,8 @@
         InodeData { mode, size: 0, data: InodeDataData::Directory(HashMap::new()) }
     }
 
-    fn new_file(zip_index: ZipIndex, zip_file: &zip::read::ZipFile) -> InodeData {
-        InodeData {
-            mode: zip_file.unix_mode().unwrap_or(DEFAULT_FILE_MODE),
-            size: zip_file.size(),
-            data: InodeDataData::File(zip_index),
-        }
+    fn new_file(zip_index: ZipIndex, mode: u32, zip_file: &zip::read::ZipFile) -> InodeData {
+        InodeData { mode, size: zip_file.size(), data: InodeDataData::File(zip_index) }
     }
 
     fn add_to_directory(&mut self, name: CString, entry: DirectoryEntry) {
@@ -188,6 +184,16 @@
 
             let mut parent = ROOT;
             let mut iter = path.iter().peekable();
+
+            let mut file_mode = DEFAULT_FILE_MODE;
+            if path.starts_with("bin/") {
+                // Allow files under bin to have execute permission, this enables payloads to bundle
+                // additional binaries that they might want to execute.
+                // An example of such binary is measure_io one used in the authfs performance tests.
+                // More context available at b/265261525 and b/270955654.
+                file_mode |= libc::S_IXUSR;
+            }
+
             while let Some(name) = iter.next() {
                 // TODO(jiyong): remove this check by canonicalizing `path`
                 if name == ".." {
@@ -211,8 +217,11 @@
                 }
 
                 // No inode found. Create a new inode and add it to the inode table.
+                // At the moment of writing this comment the apk file doesn't specify any
+                // permissions (apart from the ones on lib/), but it might change in the future.
+                // TODO(b/270955654): should we control the file permissions ourselves?
                 let inode = if is_file {
-                    InodeData::new_file(i, &file)
+                    InodeData::new_file(i, file.unix_mode().unwrap_or(file_mode), &file)
                 } else if is_leaf {
                     InodeData::new_dir(file.unix_mode().unwrap_or(DEFAULT_DIR_MODE))
                 } else {