Merge "Enforce adb unroot for testCustomVirtualMachinePermission"
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/libs/libfdt/src/lib.rs b/libs/libfdt/src/lib.rs
index 927bf50..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)
diff --git a/microdroid/Android.bp b/microdroid/Android.bp
index dc59fff..bc4db6c 100644
--- a/microdroid/Android.bp
+++ b/microdroid/Android.bp
@@ -85,13 +85,6 @@
         "microdroid_property_contexts",
         "mke2fs.microdroid",
 
-        // Adding more libs manually for unbundled build.
-        // TODO(b/268557568) these should be added automatically.
-        "libcrypto",
-        "liblzma",
-        "libc++",
-        "libssl",
-
         "libvm_payload", // used by payload to interact with microdroid manager
 
         "prng_seeder_microdroid",
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 c8d1044..4814a64 100644
--- a/microdroid/payload/Android.bp
+++ b/microdroid/payload/Android.bp
@@ -46,28 +46,3 @@
         type: "lite",
     },
 }
-
-cc_binary_host {
-    name: "mk_payload",
-    srcs: [
-        "mk_payload.cc",
-    ],
-    static_libs: [
-        "lib_microdroid_metadata_proto",
-        "libbase",
-        "libcdisk_spec",
-        "libcuttlefish_fs",
-        "libcuttlefish_host_config",
-        "libcuttlefish_utils",
-        "libext2_uuid",
-        "libimage_aggregator",
-        "libjsoncpp",
-        "liblog",
-        "libprotobuf-cpp-full",
-        "libprotobuf-cpp-lite",
-        "libsparse",
-        "libxml2",
-        "libz",
-    ],
-    static_executable: true,
-}
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_manager/src/main.rs b/microdroid_manager/src/main.rs
index 1148c31..a464163 100644
--- a/microdroid_manager/src/main.rs
+++ b/microdroid_manager/src/main.rs
@@ -721,6 +721,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())?;
diff --git a/pvmfw/Android.bp b/pvmfw/Android.bp
index 7561800..d78f4f2 100644
--- a/pvmfw/Android.bp
+++ b/pvmfw/Android.bp
@@ -13,6 +13,7 @@
     ],
     rustlibs: [
         "libaarch64_paging",
+        "libbssl_ffi_nostd",
         "libbuddy_system_allocator",
         "libdiced_open_dice_nostd",
         "libfdtpci",
@@ -21,7 +22,9 @@
         "libonce_cell_nostd",
         "libpvmfw_avb_nostd",
         "libpvmfw_embedded_key",
+        "libstatic_assertions",
         "libtinyvec_nostd",
+        "libuuid_nostd",
         "libvirtio_drivers",
         "libvmbase",
         "libzeroize_nostd",
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 14f522f..3ceb8ef 100644
--- a/pvmfw/src/dice.rs
+++ b/pvmfw/src/dice.rs
@@ -42,9 +42,9 @@
 }
 
 pub struct PartialInputs {
-    code_hash: Hash,
-    auth_hash: Hash,
-    mode: DiceMode,
+    pub code_hash: Hash,
+    pub auth_hash: Hash,
+    pub mode: DiceMode,
 }
 
 impl PartialInputs {
diff --git a/pvmfw/src/entry.rs b/pvmfw/src/entry.rs
index 964ded1..106a4ef 100644
--- a/pvmfw/src/entry.rs
+++ b/pvmfw/src/entry.rs
@@ -22,6 +22,7 @@
 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;
@@ -241,7 +242,7 @@
         RebootReason::InvalidConfig
     })?;
 
-    let bcc_slice = appended.get_bcc_mut();
+    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
@@ -252,6 +253,11 @@
     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_slice, &mut memory)?;
 
@@ -260,7 +266,7 @@
 
     // 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)
         })?;
@@ -391,17 +397,10 @@
         }
     }
 
-    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..319ff9d 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;
@@ -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/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 2e56597..d89e718 100644
--- a/pvmfw/src/main.rs
+++ b/pvmfw/src/main.rs
@@ -21,32 +21,37 @@
 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 instance;
 mod memory;
 mod mmio_guard;
 mod mmu;
+mod rand;
 mod smccc;
 mod virtio;
 
 use alloc::boxed::Box;
 
-use crate::{
-    dice::PartialInputs,
-    entry::RebootReason,
-    fdt::add_dice_node,
-    helpers::flush,
-    helpers::GUEST_PAGE_SIZE,
-    memory::MemoryTracker,
-    virtio::pci::{self, find_virtio_devices},
-};
-use diced_open_dice::{bcc_handover_main_flow, bcc_handover_parse, HIDDEN_SIZE};
+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};
@@ -81,7 +86,6 @@
     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}");
@@ -99,7 +103,14 @@
         error!("Failed to compute partial DICE inputs: {e:?}");
         RebootReason::InternalError
     })?;
-    let salt = [0; HIDDEN_SIZE]; // TODO(b/249723852): Get from instance.img and/or TRNG.
+    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
@@ -110,8 +121,9 @@
     })?;
     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/rand.rs b/pvmfw/src/rand.rs
new file mode 100644
index 0000000..a53cac6
--- /dev/null
+++ b/pvmfw/src/rand.rs
@@ -0,0 +1,99 @@
+// 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 bits = usize::try_from(u8::BITS).unwrap();
+
+    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) = hvc::trng_rnd64((chunk.len() * bits).try_into().unwrap())?;
+
+        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) = hvc::trng_rnd64((remainder.len() * bits).try_into().unwrap())?;
+
+        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(())
+}
+
+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 b61403b..58bc07e 100644
--- a/pvmfw/src/virtio/pci.rs
+++ b/pvmfw/src/virtio/pci.rs
@@ -17,11 +17,11 @@
 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::{BusDeviceIterator, PciRoot},
@@ -69,7 +69,9 @@
     Ok(())
 }
 
-struct VirtIOBlkIterator<'a> {
+pub type VirtIOBlk = blk::VirtIOBlk<HalImpl, PciTransport>;
+
+pub struct VirtIOBlkIterator<'a> {
     pci_root: &'a mut PciRoot,
     bus: BusDeviceIterator,
 }
@@ -82,7 +84,7 @@
 }
 
 impl<'a> Iterator for VirtIOBlkIterator<'a> {
-    type Item = VirtIOBlk<HalImpl, PciTransport>;
+    type Item = VirtIOBlk;
 
     fn next(&mut self) -> Option<Self::Item> {
         loop {
@@ -112,14 +114,3 @@
         }
     }
 }
-
-/// Finds VirtIO PCI devices.
-pub fn find_virtio_devices(pci_root: &mut PciRoot) -> Result<(), PciError> {
-    for mut blk in VirtIOBlkIterator::new(pci_root) {
-        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");
-    }
-
-    Ok(())
-}
diff --git a/tests/aidl/com/android/microdroid/testservice/IBenchmarkService.aidl b/tests/aidl/com/android/microdroid/testservice/IBenchmarkService.aidl
index 1eda67e..c8c8660 100644
--- a/tests/aidl/com/android/microdroid/testservice/IBenchmarkService.aidl
+++ b/tests/aidl/com/android/microdroid/testservice/IBenchmarkService.aidl
@@ -41,7 +41,4 @@
 
     /** Runs the vsock server on VM and receives data. */
     void runVsockServerAndReceiveData(int serverFd, int numBytesToReceive);
-
-    /** Adds two numbers and returns the result. */
-    int add(int a, int b);
 }
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 fd0158b..4b11d77 100644
--- a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
+++ b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
@@ -29,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;
@@ -40,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;
@@ -48,8 +51,14 @@
 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;
@@ -57,7 +66,6 @@
 import java.util.List;
 import java.util.Map;
 import java.util.OptionalLong;
-import java.util.Random;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Function;
 
@@ -72,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";
 
@@ -569,64 +578,113 @@
     }
 
     @Test
-    public void testVsockRpcBinderLatency() throws Exception {
+    public void testRpcBinderLatency() throws Exception {
+        final int NUM_WARMUPS = 10;
+        final int NUM_REQUESTS = 10_000;
+
         VirtualMachineConfig config =
                 newVmConfigBuilder()
-                        .setPayloadConfigPath("assets/vm_config_io.json")
+                        .setPayloadBinaryName("MicrodroidTestNativeLib.so")
                         .setDebugLevel(DEBUG_LEVEL_NONE)
                         .build();
 
-        List<Double> requestLatencies = new ArrayList<>(IO_TEST_TRIAL_COUNT);
+        List<Double> requestLatencies = new ArrayList<>(IO_TEST_TRIAL_COUNT * NUM_REQUESTS);
         for (int i = 0; i < IO_TEST_TRIAL_COUNT; ++i) {
-            String vmName = "test_vm_request_" + i;
-            VirtualMachine vm = forceCreateNewVirtualMachine(vmName, config);
-            BenchmarkVmListener.create(new VsockRpcBinderLatencyListener(requestLatencies))
-                    .runToFinish(TAG, vm);
+            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, "vsock/rpcbinder/request_latency", "ms");
+        reportMetrics(requestLatencies, "latency/rpcbinder", "us");
     }
 
-    private static class VsockRpcBinderLatencyListener
-            implements BenchmarkVmListener.InnerListener {
-        private static final int NUM_REQUESTS = 10000;
-        private static final int NUM_WARMUP_REQUESTS = 10;
+    @Test
+    public void testVsockLatency() throws Exception {
+        final int NUM_WARMUPS = 10;
+        final int NUM_REQUESTS = 10_000;
 
-        private final List<Double> mResults;
+        VirtualMachineConfig config =
+                newVmConfigBuilder()
+                        .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+                        .setDebugLevel(DEBUG_LEVEL_NONE)
+                        .build();
 
-        VsockRpcBinderLatencyListener(List<Double> results) {
-            mResults = results;
-        }
+        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);
 
-        @Override
-        public void onPayloadReady(VirtualMachine vm, IBenchmarkService benchmarkService)
-                throws RemoteException {
-            // Warm up a few times.
-            Random rand = new Random();
-            for (int i = 0; i < NUM_WARMUP_REQUESTS; i++) {
-                int a = rand.nextInt();
-                int b = rand.nextInt();
-                int c = benchmarkService.add(a, b);
-                assertThat(c).isEqualTo(a + b);
-            }
+                                    // Correctness check.
+                                    writer.write("hello\n");
+                                    writer.flush();
+                                    tr.mFileContent = reader.readLine().trim();
 
-            // Use the VM to compute Fibonnacci numbers, save timestamps between requests.
-            int a = 0;
-            int b = 1;
-            int c;
-            long timestamps[] = new long[NUM_REQUESTS + 1];
-            for (int i = 0; i < NUM_REQUESTS; i++) {
-                timestamps[i] = System.nanoTime();
-                c = benchmarkService.add(a, b);
-                a = b;
-                b = c;
-            }
-            timestamps[NUM_REQUESTS] = System.nanoTime();
+                                    // Warmup.
+                                    for (int j = 0; j < NUM_WARMUPS; ++j) {
+                                        String text = "test" + j + "\n";
+                                        writer.write(text);
+                                        writer.flush();
+                                        reader.readLine();
+                                    }
 
-            // Log individual request latencies.
-            for (int i = 0; i < NUM_REQUESTS; i++) {
-                long diff = timestamps[i + 1] - timestamps[i];
-                mResults.add((double) diff / NANO_TO_MILLI);
+                                    // 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/src/native/benchmarkbinary.cpp b/tests/benchmark/src/native/benchmarkbinary.cpp
index 022698f..6cfc71d 100644
--- a/tests/benchmark/src/native/benchmarkbinary.cpp
+++ b/tests/benchmark/src/native/benchmarkbinary.cpp
@@ -96,11 +96,6 @@
         return resultStatus(res);
     }
 
-    ndk::ScopedAStatus add(int32_t a, int32_t b, int32_t* out) override {
-        *out = a + b;
-        return ndk::ScopedAStatus::ok();
-    }
-
 private:
     /**
      * Measures the read rate for reading the given file.
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/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 9ec36b3..be3c1da 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;
@@ -428,4 +430,104 @@
             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 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 f1e5054..d217c00 100644
--- a/tests/hostside/Android.bp
+++ b/tests/hostside/Android.bp
@@ -22,6 +22,20 @@
     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"],
+}
+
 java_test_host {
     name: "MicrodroidHostTestCases",
     srcs: ["java/**/*.java"],
@@ -48,6 +62,8 @@
         ":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",
         "assets/bcc.dat",
     ],
     data_native_bins: [
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_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/java/com/android/microdroid/test/PvmfwDebugPolicyHostTests.java b/tests/hostside/java/com/android/microdroid/test/PvmfwDebugPolicyHostTests.java
index bda52dd..755613a 100644
--- a/tests/hostside/java/com/android/microdroid/test/PvmfwDebugPolicyHostTests.java
+++ b/tests/hostside/java/com/android/microdroid/test/PvmfwDebugPolicyHostTests.java
@@ -19,8 +19,10 @@
 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;
@@ -32,6 +34,7 @@
 import com.android.tradefed.device.TestDevice;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.util.CommandResult;
 import com.android.tradefed.util.FileUtil;
 
 import org.junit.After;
@@ -41,6 +44,7 @@
 
 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 */
@@ -50,13 +54,15 @@
     @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_LEVEL = "full";
+    @NonNull private static final String MICRODROID_DEBUG_FULL = "full";
     @NonNull private static final String MICRODROID_CONFIG_PATH = "assets/vm_config_apex.json";
-    private static final int BOOT_COMPLETE_TIMEOUT = 30000; // 30 seconds
+    @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 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 = 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";
@@ -86,6 +92,7 @@
         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();
 
@@ -162,6 +169,30 @@
                 .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();
+    }
+
     @NonNull
     private String readMicrodroidFileAsString(@NonNull String path)
             throws DeviceNotAvailableException {
@@ -184,18 +215,50 @@
                 .build();
     }
 
+    @NonNull
+    private boolean hasConsoleOutput(CommandResult result) throws DeviceNotAvailableException {
+        return result.getStdout().contains("Run /init as init process");
+    }
+
     private ITestDevice launchProtectedVmAndWaitForBootCompleted()
             throws DeviceNotAvailableException {
         mMicrodroidDevice =
                 MicrodroidBuilder.fromDevicePath(
                                 getPathForPackage(PACKAGE_NAME), MICRODROID_CONFIG_PATH)
-                        .debugLevel(MICRODROID_DEBUG_LEVEL)
+                        .debugLevel(MICRODROID_DEBUG_FULL)
                         .protectedVm(/* protectedVm= */ true)
                         .addBootFile(mCustomPvmfwBinFileOnHost, PVMFW_FILE_NAME)
                         .build(mAndroidDevice);
-        assertThat(mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT)).isTrue();
+        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/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
index a66f9c3..f3f1252 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -51,7 +51,6 @@
 import android.system.virtualmachine.VirtualMachineDescriptor;
 import android.system.virtualmachine.VirtualMachineException;
 import android.system.virtualmachine.VirtualMachineManager;
-import android.util.Log;
 
 import com.android.compatibility.common.util.CddTest;
 import com.android.microdroid.test.device.MicrodroidDeviceTestBase;
@@ -147,6 +146,7 @@
 
         TestResults testResults =
                 runVmTestService(
+                        TAG,
                         vm,
                         (ts, tr) -> {
                             tr.mAddInteger = ts.addInteger(123, 456);
@@ -192,7 +192,7 @@
         VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config);
 
         TestResults testResults =
-                runVmTestService(vm, (ts, tr) -> tr.mAddInteger = ts.addInteger(37, 73));
+                runVmTestService(TAG, vm, (ts, tr) -> tr.mAddInteger = ts.addInteger(37, 73));
         testResults.assertNoException();
         assertThat(testResults.mAddInteger).isEqualTo(37 + 73);
     }
@@ -375,6 +375,7 @@
 
         TestResults testResults =
                 runVmTestService(
+                        TAG,
                         vm,
                         (service, results) -> {
                             service.runEchoReverseServer();
@@ -687,7 +688,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");
     }
@@ -772,6 +774,7 @@
 
         TestResults testResults =
                 runVmTestService(
+                        TAG,
                         vm,
                         (ts, tr) -> {
                             tr.mApkContentsPath = ts.getApkContentsPath();
@@ -807,6 +810,7 @@
 
         TestResults testResults =
                 runVmTestService(
+                        TAG,
                         vm,
                         (ts, tr) -> {
                             tr.mExtraApkTestProp =
@@ -994,6 +998,7 @@
         VirtualMachine vm = forceCreateNewVirtualMachine("bcc_vm", normalConfig);
         TestResults testResults =
                 runVmTestService(
+                        TAG,
                         vm,
                         (service, results) -> {
                             results.mBcc = service.getBcc();
@@ -1279,6 +1284,7 @@
         // 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);
@@ -1305,6 +1311,7 @@
         assertThat(vmImport).isEqualTo(vmm.get(vmNameImport));
         TestResults testResults =
                 runVmTestService(
+                        TAG,
                         vmImport,
                         (ts, tr) -> {
                             tr.mAddInteger = ts.addInteger(123, 456);
@@ -1331,6 +1338,7 @@
 
         TestResults testResults =
                 runVmTestService(
+                        TAG,
                         vm,
                         (ts, tr) -> {
                             tr.mEncryptedStoragePath = ts.getEncryptedStoragePath();
@@ -1355,6 +1363,7 @@
 
         TestResults testResults =
                 runVmTestService(
+                        TAG,
                         vm,
                         (ts, tr) -> {
                             ts.writeToFile(
@@ -1413,6 +1422,7 @@
 
         final TestResults testResults =
                 runVmTestService(
+                        TAG,
                         vm,
                         (ts, tr) -> {
                             tr.mEffectiveCapabilities = ts.getEffectiveCapabilities();
@@ -1437,6 +1447,7 @@
         VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_a", config);
         TestResults testResults =
                 runVmTestService(
+                        TAG,
                         vm,
                         (ts, tr) -> {
                             ts.writeToFile(
@@ -1449,6 +1460,7 @@
         // stopped the VM
         testResults =
                 runVmTestService(
+                        TAG,
                         vm,
                         (ts, tr) -> {
                             tr.mFileContent = ts.readFromFile("/mnt/encryptedstore/test_file");
@@ -1472,6 +1484,7 @@
 
         TestResults testResults =
                 runVmTestService(
+                        TAG,
                         vm,
                         (testService, ts) -> {
                             ts.mFileContent = testService.readFromFile("/mnt/apk/assets/file.txt");
@@ -1516,7 +1529,7 @@
                         .build();
         final VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_logcat", vmConfig);
 
-        runVmTestService(vm, (service, results) -> {});
+        runVmTestService(TAG, vm, (service, results) -> {});
 
         // only check logs printed after this test
         Process logcatProcess =
@@ -1565,6 +1578,7 @@
         try (VirtualMachine vm = forceCreateNewVirtualMachine("vm_from_another_app", config)) {
             TestResults results =
                     runVmTestService(
+                            TAG,
                             vm,
                             (ts, tr) -> {
                                 tr.mAddInteger = ts.addInteger(101, 303);
@@ -1587,7 +1601,7 @@
 
         VirtualMachine originalVm = forceCreateNewVirtualMachine("original_vm", config);
         // Just start & stop the VM.
-        runVmTestService(originalVm, (ts, tr) -> {});
+        runVmTestService(TAG, originalVm, (ts, tr) -> {});
 
         // Now create the descriptor and manually parcel & unparcel it.
         VirtualMachineDescriptor vmDescriptor = toParcelFromParcel(originalVm.toDescriptor());
@@ -1606,7 +1620,7 @@
                 "instance.img", "original_vm", "import_vm_from_unparceled");
 
         // Check that we can start and stop imported vm as well
-        runVmTestService(importVm, (ts, tr) -> {});
+        runVmTestService(TAG, importVm, (ts, tr) -> {});
     }
 
     @Test
@@ -1625,6 +1639,7 @@
         {
             TestResults testResults =
                     runVmTestService(
+                            TAG,
                             originalVm,
                             (ts, tr) -> {
                                 ts.writeToFile("not a secret!", "/mnt/encryptedstore/secret.txt");
@@ -1652,6 +1667,7 @@
 
         TestResults testResults =
                 runVmTestService(
+                        TAG,
                         importVm,
                         (ts, tr) -> {
                             tr.mFileContent = ts.readFromFile("/mnt/encryptedstore/secret.txt");
@@ -1677,7 +1693,7 @@
 
         VirtualMachine vm = forceCreateNewVirtualMachine("vm_to_share", config);
         // Just start & stop the VM.
-        runVmTestService(vm, (ts, tr) -> {});
+        runVmTestService(TAG, vm, (ts, tr) -> {});
         // Get a descriptor that we will share with another app (VM_SHARE_APP_PACKAGE_NAME)
         VirtualMachineDescriptor vmDesc = vm.toDescriptor();
 
@@ -1686,6 +1702,7 @@
                 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);
@@ -1726,6 +1743,7 @@
         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");
@@ -1738,6 +1756,7 @@
                 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);
@@ -1835,102 +1854,4 @@
                 .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;
-        byte[] mBcc;
-
-        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);
-            }
-        }
-    }
-
-    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));
-                            // Make sure linkToDeath works, and include it in the log in case it's
-                            // helpful.
-                            mTestService
-                                    .asBinder()
-                                    .linkToDeath(() -> Log.i(TAG, "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(TAG, "onPayloadReady");
-                        payloadReady.complete(true);
-                        testVMService(vm);
-                        quitVMService();
-                    }
-
-                    @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/testbinary.cpp b/tests/testapk/src/native/testbinary.cpp
index 365ea75..19d6cd4 100644
--- a/tests/testapk/src/native/testbinary.cpp
+++ b/tests/testapk/src/native/testbinary.cpp
@@ -84,28 +84,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() {
diff --git a/virtualizationmanager/src/aidl.rs b/virtualizationmanager/src/aidl.rs
index aceb319..5cd523d 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;
@@ -317,6 +319,12 @@
             check_gdb_allowed(config)?;
         }
 
+        let ramdump = if is_ramdump_needed(config) {
+            Some(prepare_ramdump_file(&temporary_directory)?)
+        } else {
+            None
+        };
+
         let state = &mut *self.state.lock().unwrap();
         let console_fd =
             clone_or_prepare_logger_fd(config, console_fd, format!("Console({})", cid))?;
@@ -407,19 +415,6 @@
             }
         };
 
-        // 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)),
-            )
-        })?;
-
         // Actually start the VM.
         let crosvm_config = CrosvmConfig {
             cid,
@@ -436,7 +431,7 @@
             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,
@@ -485,10 +480,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;
@@ -978,11 +969,20 @@
     })
 }
 
-fn is_debuggable(config: &VirtualMachineConfig) -> bool {
-    match config {
-        VirtualMachineConfig::AppConfig(config) => config.debugLevel != DebugLevel::NONE,
-        _ => false,
-    }
+/// 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 {
@@ -1031,9 +1031,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/crosvm.rs b/virtualizationmanager/src/crosvm.rs
index 1456d17..6ac1658 100644
--- a/virtualizationmanager/src/crosvm.rs
+++ b/virtualizationmanager/src/crosvm.rs
@@ -68,6 +68,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;
 
@@ -665,24 +667,6 @@
     }
 }
 
-fn should_configure_ramdump(protected: bool) -> bool {
-    if protected {
-        // Protected VM needs ramdump configuration here.
-        // pvmfw will disable ramdump if unnecessary.
-        true
-    } else {
-        // For unprotected VM, ramdump should be handled here.
-        // ramdump wouldn't be enabled if ramdump is explicitly set to <1>.
-        if let Ok(mut file) = File::open("/proc/device-tree/avf/guest/common/ramdump") {
-            let mut ramdump: [u8; 4] = Default::default();
-            file.read_exact(&mut ramdump).map_err(|_| false).unwrap();
-            // DT spec uses big endian although Android is always little endian.
-            return u32::from_be_bytes(ramdump) == 1;
-        }
-        false
-    }
-}
-
 /// Starts an instance of `crosvm` to manage a new VM.
 fn run_vm(
     config: CrosvmConfig,
@@ -722,12 +706,22 @@
         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 let Some(memory_mib) = config.memory_mib {
@@ -816,10 +810,6 @@
     debug!("Preserving FDs {:?}", preserved_fds);
     command.preserved_fds(preserved_fds);
 
-    if should_configure_ramdump(config.protected) {
-        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..a4ec419
--- /dev/null
+++ b/virtualizationmanager/src/debug_config.rs
@@ -0,0 +1,58 @@
+// 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()
+}
+
+/// 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 (protected, debuggable) = match config {
+        VirtualMachineConfig::RawConfig(config) => {
+            // custom VMs are considered debuggable for flexibility
+            (config.protectedVm, true)
+        }
+        VirtualMachineConfig::AppConfig(config) => {
+            (config.protectedVm, config.debugLevel == DebugLevel::FULL)
+        }
+    };
+
+    if protected {
+        enabled_in_dp
+    } else {
+        enabled_in_dp || debuggable
+    }
+}
diff --git a/virtualizationmanager/src/main.rs b/virtualizationmanager/src/main.rs
index 3f0b64b..bd7f8af 100644
--- a/virtualizationmanager/src/main.rs
+++ b/virtualizationmanager/src/main.rs
@@ -18,6 +18,7 @@
 mod atom;
 mod composite;
 mod crosvm;
+mod debug_config;
 mod payload;
 mod selinux;
 
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()
+}