Merge "Fix missing perf-setup upon running benchmark" into main
diff --git a/README.md b/README.md
index eb28e94..eaa2579 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,29 @@
-# Virtualization
+# Android Virtualization Framework (AVF)
 
-This repository contains userspace services related to running virtual machines on Android,
-especially protected virtual machines. See the
-[getting started documentation](docs/getting_started/index.md) and
-[Microdroid README](microdroid/README.md) for more information.
+Android Virtualization Framework (AVF) provides secure and private execution environments for
+executing code. AVF is ideal for security-oriented use cases that require stronger isolation
+assurances over those offered by Android’s app sandbox.
+
+Visit [our public doc site](https://source.android.com/docs/core/virtualization) to learn more about
+what AVF is, what it is for, and how it is structured. This repository contains source code for
+userspace components of AVF.
+
+If you want a quick start, see the [getting started guideline](docs/getting_started.md)
+and follow the steps there.
+
+For in-depth explanations about individual topics and components, visit the following links.
+
+AVF components:
+
+* [pVM firmware](pvmfw/README.md)
+* [Microdroid](microdroid/README.md)
+* [Microdroid kernel](microdroid/kernel/README.md)
+* [Microdroid payload](microdroid/payload/README.md)
+* [vmbase](vmbase/README.md)
+* [VM Payload API](vm_payload/README.md)
+
+How-Tos:
+* [Building and running a demo app in Java](demo/README.md)
+* [Building and running a demo app in C++](demo_native/README.md)
+* [Debugging](docs/debug)
+* [Using custom VM](docs/custom_vm.md)
diff --git a/docs/custom_vm.md b/docs/custom_vm.md
new file mode 100644
index 0000000..270ea36
--- /dev/null
+++ b/docs/custom_vm.md
@@ -0,0 +1,23 @@
+# Custom VM
+
+You can spawn your own custom VMs by passing a JSON config file to the
+VirtualizationService via the `vm` tool on a rooted AVF-enabled device. If your
+device is attached over ADB, you can run:
+
+```shell
+cat > vm_config.json <<EOF
+{
+  "kernel": "/data/local/tmp/kernel",
+  "initrd": "/data/local/tmp/ramdisk",
+  "params": "rdinit=/bin/init"
+}
+EOF
+adb root
+adb push <kernel> /data/local/tmp/kernel
+adb push <ramdisk> /data/local/tmp/ramdisk
+adb push vm_config.json /data/local/tmp/vm_config.json
+adb shell "/apex/com.android.virt/bin/vm run /data/local/tmp/vm_config.json"
+```
+
+The `vm` command also has other subcommands for debugging; run
+`/apex/com.android.virt/bin/vm help` for details.
diff --git a/docs/debug/README.md b/docs/debug/README.md
new file mode 100644
index 0000000..b7729a4
--- /dev/null
+++ b/docs/debug/README.md
@@ -0,0 +1,119 @@
+# Debugging protected VMs
+
+AVF is largely about protected VMs. This in turn means that anything that is
+happening inside the VM cannot be observed from outside of the VM. But as a
+developer, you need to be able to look into it when there’s an error in your
+VM. To satisfy such contradictory needs, AVF allows you to start a protected VM
+in a debuggable mode and provides a bunch of debugging mechanisms you can use
+to better understand the behavior of the VM and diagnose issues.
+
+Note: running a protected VM in a debuggable mode introduces many loopholes
+which can be used to nullify the protection provided by the hypervisor.
+Therefore, the debugable mode should never be used in production.
+
+## Enable debugging
+
+The following sections describe the two ways debugging can be enabled.
+
+### Debug level
+
+Debug level is a per-VM property which indicates how debuggable the VM is.
+There currently are two levels defined: NONE and FULL. NONE means that the VM
+is not debuggable at all, and FULL means that [all the debugging
+features](#debugging-features) are supported.
+
+Debug level is by default NONE. You can set it to FULL either via a Java API
+call in your app or via a command line argument `--debug` as follows:
+
+```java
+VirtualMachineConfig.Builder.setDebugLevel(DEBUG_LEVEL_FULL);
+```
+
+or
+
+```shell
+adb shell /apex/com.android.virt/bin/vm run-microdroid --debug full
+```
+
+or
+
+```shell
+m vm_shell
+vm_shell start-microdroid --auto-connect -- --protected --debug full
+```
+
+Note: `--debug full` is the default option when omitted. You need to explicitly
+use `--debug none` to set the debug level to NONE.
+
+### Debug policy
+
+Debug policy is a per-device property which forcibly enables selected debugging
+features, even for the VMs with debug level NONE.
+
+The main purpose of debug policy is in-field debugging by the platform
+developers (device makers, SoC vendors, etc.) To understand it, let’s imagine
+that you have an application of pVM. It’s configured as debug level NONE
+because you finished the development and the team-level testing. However, you
+get a bug report from your QA team or from beta testers. To fix the bug, you
+should be able to look into the pVM but you do not want to change the source
+code to make the VM debuggable and rebuild the entire software, because that
+may hurt the reproducibility of the bug.
+
+Note: Not every devices is guaranteed to support debug policy. It is up to the
+device manufacturer to implement this in their bootloader. Google Pixel
+devices for example support this after Pixel 7 and 7 Pro. Pixel 6 and 6 Pro
+don't support debug policy.
+
+In the Pixel phones supporting debug policy, it is provisioned by installing a
+device tree overlay like below to the Pixel-specific partition `dpm`.
+
+```
+/ {
+    fragment@avf {
+        target-path = "/";
+
+        __overlay__ {
+            avf {
+                common {
+                    log = <1>; // Enable kernel log and logcat
+                    ramdump = <1>; // Enable ramdump
+                }
+                microdroid {
+                    adb = <1>; // Enable ADB connection
+                }
+            };
+        };
+    };
+}; /* end of avf */
+```
+
+To not enable a specific debugging feature, set the corresponding property
+value to other than `<1>`, or delete the property.
+
+As a reference, in Pixel phones, debug policy is loaded as below:
+
+1. Bootloader loads it from the `dpm` partition and verifies it.
+1. Bootloader appends the loaded debug policy as the [configuration
+   data](../../pvmfw/README.md#configuration-data) of the pvmfw.
+1. When a pVM is started, pvmfw [overlays][apply_debug_policy] the debug policy to the baseline
+   device tree from crosvm.
+1. OS payload (e.g. Microdroid) [reads][read_debug_policy] the device tree and enables specific
+   debugging feature accordingly.
+
+**Note**: Bootloader MUST NOT load debug policy when the bootloader is in LOCKED state.
+
+[apply_debug_policy]: https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Virtualization/pvmfw/src/fdt.rs;drc=0d52747770baa14d44c0779b5505095b4251f2e9;l=790
+[read_debug_policy]: https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Virtualization/microdroid_manager/src/main.rs;drc=65c9f1f0eee4375535f2025584646a0dbb0ea25c;l=834
+
+## Debugging features
+
+AVF currently supports the following debugging features:
+
+* ADB connection (only for Microdroid)
+* Capturing console output
+* Capturing logcat output (only for Microdroid)
+* [Capturing kernel ramdump](ramdump.md) (only for Microdroid)
+* Capturing userspace crash dump (only for Microdroid)
+* [Attaching GDB to the kernel](gdb_kernel.md)
+* [Attaching GDB to the userspace process](gdb_userspace.md) (only for Microdroid)
+* [Tracing hypervisor events](tracing.md)
diff --git a/docs/debug/gdb.md b/docs/debug/gdb_kernel.md
similarity index 100%
rename from docs/debug/gdb.md
rename to docs/debug/gdb_kernel.md
diff --git a/docs/debug/gdb_userspace.md b/docs/debug/gdb_userspace.md
new file mode 100644
index 0000000..c8af702
--- /dev/null
+++ b/docs/debug/gdb_userspace.md
@@ -0,0 +1,18 @@
+# Debugging the payload on microdroid
+
+Like a normal adb device, you can debug native processes running on a
+Microdroid-base VM using [`lldbclient.py`][lldbclient] script, either by
+running a new process, or attaching to an existing process.  Use `vm_shell`
+tool above, and then run `lldbclient.py`.
+
+```sh
+adb -s localhost:8000 shell 'mount -o remount,exec /data'
+development/scripts/lldbclient.py -s localhost:8000 --chroot . --user '' \
+    (-p PID | -n NAME | -r ...)
+```
+
+**Note:** We need to pass `--chroot .` to skip verifying device, because
+microdroid doesn't match with the host's lunch target. We need to also pass
+`--user ''` as there is no `su` binary in microdroid.
+
+[lldbclient]: https://android.googlesource.com/platform/development/+/refs/heads/main/scripts/lldbclient.py
diff --git a/docs/getting_started.md b/docs/getting_started.md
new file mode 100644
index 0000000..d970c12
--- /dev/null
+++ b/docs/getting_started.md
@@ -0,0 +1,156 @@
+# Getting started with Android Virtualization Framework
+
+## Step 1: Prepare a device
+
+We support the following devices:
+
+* aosp\_panther (Pixel 7)
+* aosp\_cheetah (Pixel 7 Pro)
+* aosp\_oriole (Pixel 6)
+* aosp\_raven (Pixel 6 Pro)
+* aosp\_felix (Pixel Fold)
+* aosp\_tangopro (Pixel Tablet)
+* aosp\_cf\_x86\_64\_phone (Cuttlefish a.k.a. Cloud Android). Follow [this
+  instruction](https://source.android.com/docs/setup/create/cuttlefish-use) to
+  use.
+
+### Note on Pixel 6 and 6 Pro
+AVF is shipped in Pixel 6 and 6 Pro, but isn't enabled by default. To enable
+it, follow the instructions below:
+
+1. If the device is running Android 13 or earlier, upgrade to Android 14.
+
+1. Once upgraded to Android 14, execute the following command to enable pKVM.
+   ```shell
+   adb reboot bootloader
+   fastboot flashing unlock
+   fastboot oem pkvm enable
+   fastboot reboot
+   ```
+### Note on Cuttlefish
+Cuttlefish does not support protected VMs. Only non-protected VMs are
+supported.
+
+## Step 2: Build Android image
+
+This step is optional unless you want to build AVF by yourself or try the
+in-development version of AVF.
+
+AVF is implemented as an APEX named `com.android.virt`. However, in order for
+you to install it to your device (be it Pixel or Cuttlefish), you first need to
+re-build the entire Android from AOSP. This is because the official Android
+build you have in your device is release-key signed and therefore you can't
+install your custom-built AVF APEX to it - because it is test-key signed.
+
+### Pixel
+
+1. [Download](https://source.android.com/docs/setup/download/downloading)
+   source code from AOSP. Use the `main` branch.
+
+1. [Download](https://developers.google.com/android/blobs-preview) the preview
+   vendor blob that matches your device.
+
+1. [Build](https://source.android.com/docs/setup/build/building) the `aosp_`
+   variant of your device. For example, if your device is Pixel 7 (`panther`),
+   build `aosp_panther`.
+
+1. [Flash](https://source.android.com/docs/setup/build/running) the built
+   images to the device.
+
+
+### Cuttlefish
+
+1. [Download](https://source.android.com/docs/setup/download/downloading)
+   source code from AOSP. Use the `main` branch.
+
+1. Build Cuttlefish:
+   ```shell
+   source build/envsetup.sh
+   lunch aosp_cf_x86_64_phone-userdebug
+   m
+   ```
+
+1. Run Cuttlefish:
+   ```shell
+   cvd start
+   ```
+
+## Step 3: Build AVF
+
+Then you can repeat building and installing AVF to the device as follows:
+
+1. Build the AVF APEX.
+   ```sh
+   banchan com.android.virt aosp_arm64
+   UNBUNDLED_BUILD_SDKS_FROM_SOURCE=true m apps_only dist
+   ```
+   Replace `aosp_arm64` with `aosp_x86_64` if you are building for Cuttlefish.
+
+1. Install the AVF APEX to the device.
+   ```sh
+   adb install out/dist/com.android.virt.apex
+   adb reboot; adb wait-for-device
+   ```
+
+## Step 4: Run a Microdroid VM
+
+[Microdroid](../../microdroid/README.md) is a lightweight version of Android
+that is intended to run on pVM. You can run a Microdroid-based VM with an empty
+payload using the following command:
+
+```shell
+package/modules/Virtualization/vm/vm_shell.sh start-microdroid --auto-connect -- --protected
+```
+
+You will see the log messages like the below.
+
+```
+found path /apex/com.android.virt/app/EmptyPayloadAppGoogle@MASTER/EmptyPayloadAppGoogle.apk
+creating work dir /data/local/tmp/microdroid/7CI6QtktSluD3OZgv
+apk.idsig path: /data/local/tmp/microdroid/7CI6QtktSluD3OZgv/apk.idsig
+instance.img path: /data/local/tmp/microdroid/7CI6QtktSluD3OZgv/instance.img
+Created VM from "/apex/com.android.virt/app/EmptyPayloadAppGoogle@MASTER/EmptyPayloadAppGoogle.apk"!PayloadConfig(VirtualMachinePayloadConfig { payloadBinaryName: "MicrodroidEmptyPayloadJniLib.so" }) with CID 2052, state is STARTING.
+[2023-07-07T14:50:43.420766770+09:00 INFO  crosvm] crosvm started.
+[2023-07-07T14:50:43.422545090+09:00 INFO  crosvm] CLI arguments parsed.
+[2023-07-07T14:50:43.440984015+09:00 INFO  crosvm::crosvm::sys::unix::device_helpers] Trying to attach block device: /proc/self/fd/49
+[2023-07-07T14:50:43.441730922+09:00 INFO  crosvm::crosvm::sys::unix::device_helpers] Trying to attach block device: /proc/self/fd/54
+[2023-07-07T14:50:43.462055141+09:00 INFO  crosvm::crosvm::sys::unix::device_helpers] Trying to attach block device: /proc/self/fd/63
+[WARN] Config entry DebugPolicy uses non-zero offset with zero size
+[WARN] Config entry DebugPolicy uses non-zero offset with zero size
+[INFO] pVM firmware
+avb_slot_verify.c:443: ERROR: initrd_normal: Hash of data does not match digest in descriptor.
+[INFO] device features: SEG_MAX | RO | BLK_SIZE | RING_EVENT_IDX | VERSION_1 | ACCESS_PLATFORM
+[INFO] config: 0x201a000
+[INFO] found a block device of size 50816KB
+[INFO] device features: SEG_MAX | BLK_SIZE | FLUSH | DISCARD | WRITE_ZEROES | RING_EVENT_IDX | VERSION_1 | ACCESS_PLATFORM
+[INFO] config: 0x2022000
+[INFO] found a block device of size 10304KB
+[INFO] No debug policy found.
+[INFO] Starting payload...
+<omitted>
+07-07 05:52:01.322    69    69 I vm_payload: vm_payload: Notified host payload ready successfully
+07-07 05:52:01.364    70    70 I adbd    : persist.adb.watchdog set to ''
+07-07 05:52:01.364    70    70 I adbd    : persist.sys.test_harness set to ''
+07-07 05:52:01.365    70    70 I adbd    : adb watchdog timeout set to 600 seconds
+07-07 05:52:01.366    70    70 I adbd    : Setup mdns on port= 5555
+07-07 05:52:01.366    70    70 I adbd    : adbd listening on vsock:5555
+07-07 05:52:01.366    70    70 I adbd    : adbd started
+#
+```
+
+The `--auto-connect` option provides you an adb-shell connection to the VM. The
+shell promot (`#`) at the end of the log is for that.
+
+## Step 5: Run tests
+
+There are various tests that spawn guest VMs and check different aspects of the
+architecture. They all can run via `atest`.
+
+```shell
+atest MicrodroidHostTestCases
+atest MicrodroidTestApp
+```
+
+If you run into problems, inspect the logs produced by `atest`. Their location
+is printed at the end. The `host_log_*.zip` file should contain the output of
+individual commands as well as VM logs.
diff --git a/docs/getting_started/index.md b/docs/getting_started/index.md
deleted file mode 100644
index 9dcd4fa..0000000
--- a/docs/getting_started/index.md
+++ /dev/null
@@ -1,126 +0,0 @@
-# Getting started with Protected Virtual Machines
-
-## Prepare a device
-
-First you will need a device that is capable of running virtual machines. On arm64, this means a
-device which boots the kernel in EL2 and the kernel was built with KVM enabled. Unfortunately at the
-moment, we don't have an arm64 device in AOSP which does that. Instead, use cuttlefish which
-provides the same functionalities except that the virtual machines are not protected from the host
-(i.e. Android). This however should be enough for functional testing.
-
-We support the following device:
-
-* aosp_cf_x86_64_phone (Cuttlefish a.k.a. Cloud Android)
-* oriole/raven (Pixel 6, and 6 Pro)
-* panther/cheetah (Pixel 7, and 7 Pro)
-
-### Cuttlefish
-
-Building Cuttlefish
-
-```shell
-source build/envsetup.sh
-lunch aosp_cf_x86_64_phone-userdebug
-m
-```
-
-Run Cuttlefish locally by
-
-```shell
-acloud create --local-instance --local-image
-```
-
-### Google Pixel phones
-
-If the device is running Android 13 or earlier, join the [Android Beta
-Program](https://developer.android.com/about/versions/14/get#on_pixel) to upgrade to Android 14
-Beta.
-
-Once upgraded to Android 14, and if you are using Pixel 6 or 6 Pro, execute the following command to
-enable pKVM. You don't need to do this for Pixel 7 and 7 Pro.
-
-```shell
-adb reboot bootloader
-fastboot flashing unlock
-fastboot oem pkvm enable
-fastboot reboot
-```
-
-## Running demo app
-
-The instruction is [here](../../demo/README.md).
-
-## Running tests
-
-There are various tests that spawn guest VMs and check different aspects of the architecture. They
-all can run via `atest`.
-
-```shell
-atest MicrodroidHostTestCases
-atest MicrodroidTestApp
-```
-
-If you run into problems, inspect the logs produced by `atest`. Their location is printed at the
-end. The `host_log_*.zip` file should contain the output of individual commands as well as VM logs.
-
-## Spawning your own VMs with custom kernel
-
-You can spawn your own VMs by passing a JSON config file to the VirtualizationService via the `vm`
-tool on a rooted KVM-enabled device. If your device is attached over ADB, you can run:
-
-```shell
-cat > vm_config.json
-{
-  "kernel": "/data/local/tmp/kernel",
-  "initrd": "/data/local/tmp/ramdisk",
-  "params": "rdinit=/bin/init"
-}
-adb root
-adb push <kernel> /data/local/tmp/kernel
-adb push <ramdisk> /data/local/tmp/ramdisk
-adb push vm_config.json /data/local/tmp/vm_config.json
-adb shell "start virtualizationservice"
-adb shell "/apex/com.android.virt/bin/vm run /data/local/tmp/vm_config.json"
-```
-
-The `vm` command also has other subcommands for debugging; run `/apex/com.android.virt/bin/vm help`
-for details.
-
-## Spawning your own VMs with custom pvmfw
-
-Set system property `hypervisor.pvmfw.path` to custom `pvmfw` on the device before using `vm` tool.
-`virtualizationservice` will pass the specified `pvmfw` to `crosvm` for protected VMs.
-
-```shell
-adb push pvmfw.img /data/local/tmp/pvmfw.img
-adb root  # required for setprop
-adb shell setprop hypervisor.pvmfw.path /data/local/tmp/pvmfw.img
-```
-
-## Spawning your own VMs with Microdroid
-
-[Microdroid](../../microdroid/README.md) is a lightweight version of Android that is intended to run
-on pVM. You can run a Microdroid with empty payload using the following command:
-
-```shell
-adb shell /apex/com.android.virt/bin/vm run-microdroid
-```
-
-which spawns a "debuggable" VM by default to allow access to guest kernel logs.
-To run a production non-debuggable VM, pass `--debug none`.
-
-## Building and updating CrosVM and VirtualizationService {#building-and-updating}
-
-You can update CrosVM and the VirtualizationService by updating the `com.android.virt` APEX instead
-of rebuilding the entire image.
-
-```shell
-banchan com.android.virt aosp_arm64   // or aosp_x86_64 if the device is cuttlefish
-UNBUNDLED_BUILD_SDKS_FROM_SOURCE=true m apps_only dist
-adb install out/dist/com.android.virt.apex
-adb reboot
-```
-
-## Building and updating kernel inside Microdroid
-
-The instruction is [here](../../microdroid/kernel/README.md).
diff --git a/libs/fdtpci/src/lib.rs b/libs/fdtpci/src/lib.rs
index 96d98d6..602f736 100644
--- a/libs/fdtpci/src/lib.rs
+++ b/libs/fdtpci/src/lib.rs
@@ -119,7 +119,9 @@
     /// method must only be called once, and there must be no other `PciRoot` constructed using the
     /// same CAM.
     pub unsafe fn make_pci_root(&self) -> PciRoot {
-        PciRoot::new(self.cam_range.start as *mut u8, Cam::MmioCam)
+        // SAFETY: We trust that the FDT gave us a valid MMIO base address for the CAM. The caller
+        // guarantees to only call us once, so there are no other references to it.
+        unsafe { PciRoot::new(self.cam_range.start as *mut u8, Cam::MmioCam) }
     }
 }
 
diff --git a/libs/hyp/src/error.rs b/libs/hyp/src/error.rs
index b8498ca..3fdad70 100644
--- a/libs/hyp/src/error.rs
+++ b/libs/hyp/src/error.rs
@@ -26,7 +26,7 @@
 #[derive(Debug, Clone)]
 pub enum Error {
     /// MMIO guard is not supported.
-    MmioGuardNotsupported,
+    MmioGuardNotSupported,
     /// Failed to invoke a certain KVM HVC function.
     KvmError(KvmError, u32),
     /// Failed to invoke GenieZone HVC function.
@@ -40,7 +40,7 @@
 impl fmt::Display for Error {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         match self {
-            Self::MmioGuardNotsupported => write!(f, "MMIO guard is not supported"),
+            Self::MmioGuardNotSupported => write!(f, "MMIO guard is not supported"),
             Self::KvmError(e, function_id) => {
                 write!(f, "Failed to invoke the HVC function with function ID {function_id}: {e}")
             }
diff --git a/libs/hyp/src/hypervisor/common.rs b/libs/hyp/src/hypervisor/common.rs
index 7c030a1..70fdd0a 100644
--- a/libs/hyp/src/hypervisor/common.rs
+++ b/libs/hyp/src/hypervisor/common.rs
@@ -14,7 +14,7 @@
 
 //! This module regroups some common traits shared by all the hypervisors.
 
-use crate::error::Result;
+use crate::error::{Error, Result};
 use crate::util::SIZE_4KB;
 
 /// Expected MMIO guard granule size, validated during MMIO guard initialization.
@@ -34,10 +34,9 @@
 }
 
 pub trait MmioGuardedHypervisor {
-    /// Initializes the hypervisor by enrolling a MMIO guard and checking the memory granule size.
-    /// By enrolling, all MMIO will be blocked unless allow-listed with `mmio_guard_map`.
-    /// Protected VMs are auto-enrolled.
-    fn init(&self) -> Result<()>;
+    /// Enrolls with the MMIO guard so that all MMIO will be blocked unless allow-listed with
+    /// `MmioGuardedHypervisor::map`.
+    fn enroll(&self) -> Result<()>;
 
     /// Maps a page containing the given memory address to the hypervisor MMIO guard.
     /// The page size corresponds to the MMIO guard granule size.
@@ -46,6 +45,18 @@
     /// Unmaps a page containing the given memory address from the hypervisor MMIO guard.
     /// The page size corresponds to the MMIO guard granule size.
     fn unmap(&self, addr: usize) -> Result<()>;
+
+    /// Returns the MMIO guard granule size in bytes.
+    fn granule(&self) -> Result<usize>;
+
+    // TODO(ptosi): Fully move granule validation to client code.
+    /// Validates the MMIO guard granule size.
+    fn validate_granule(&self) -> Result<()> {
+        match self.granule()? {
+            MMIO_GUARD_GRANULE_SIZE => Ok(()),
+            granule => Err(Error::UnsupportedMmioGuardGranule(granule)),
+        }
+    }
 }
 
 pub trait MemSharingHypervisor {
diff --git a/libs/hyp/src/hypervisor/geniezone.rs b/libs/hyp/src/hypervisor/geniezone.rs
index 24eb89e..ad18e17 100644
--- a/libs/hyp/src/hypervisor/geniezone.rs
+++ b/libs/hyp/src/hypervisor/geniezone.rs
@@ -14,9 +14,7 @@
 
 //! Wrappers around calls to the GenieZone hypervisor.
 
-use super::common::{
-    Hypervisor, MemSharingHypervisor, MmioGuardedHypervisor, MMIO_GUARD_GRANULE_SIZE,
-};
+use super::common::{Hypervisor, MemSharingHypervisor, MmioGuardedHypervisor};
 use crate::error::{Error, Result};
 use crate::util::page_address;
 use core::fmt::{self, Display, Formatter};
@@ -96,13 +94,15 @@
 }
 
 impl MmioGuardedHypervisor for GeniezoneHypervisor {
-    fn init(&self) -> Result<()> {
-        mmio_guard_enroll()?;
-        let mmio_granule = mmio_guard_granule()?;
-        if mmio_granule != MMIO_GUARD_GRANULE_SIZE {
-            return Err(Error::UnsupportedMmioGuardGranule(mmio_granule));
+    fn enroll(&self) -> Result<()> {
+        let args = [0u64; 17];
+        match success_or_error_64(hvc64(VENDOR_HYP_GZVM_MMIO_GUARD_ENROLL_FUNC_ID, args)[0]) {
+            Ok(()) => Ok(()),
+            Err(GeniezoneError::NotSupported) | Err(GeniezoneError::NotRequired) => {
+                Err(Error::MmioGuardNotSupported)
+            }
+            Err(e) => Err(Error::GeniezoneError(e, VENDOR_HYP_GZVM_MMIO_GUARD_ENROLL_FUNC_ID)),
         }
-        Ok(())
     }
 
     fn map(&self, addr: usize) -> Result<()> {
@@ -118,6 +118,12 @@
 
         checked_hvc64_expect_zero(VENDOR_HYP_GZVM_MMIO_GUARD_UNMAP_FUNC_ID, args)
     }
+
+    fn granule(&self) -> Result<usize> {
+        let args = [0u64; 17];
+        let granule = checked_hvc64(VENDOR_HYP_GZVM_MMIO_GUARD_INFO_FUNC_ID, args)?;
+        Ok(granule.try_into().unwrap())
+    }
 }
 
 impl MemSharingHypervisor for GeniezoneHypervisor {
@@ -142,23 +148,6 @@
     }
 }
 
-fn mmio_guard_granule() -> Result<usize> {
-    let args = [0u64; 17];
-
-    let granule = checked_hvc64(VENDOR_HYP_GZVM_MMIO_GUARD_INFO_FUNC_ID, args)?;
-    Ok(granule.try_into().unwrap())
-}
-
-fn mmio_guard_enroll() -> Result<()> {
-    let args = [0u64; 17];
-    match success_or_error_64(hvc64(VENDOR_HYP_GZVM_MMIO_GUARD_ENROLL_FUNC_ID, args)[0]) {
-        Ok(_) => Ok(()),
-        Err(GeniezoneError::NotSupported) => Err(Error::MmioGuardNotsupported),
-        Err(GeniezoneError::NotRequired) => Err(Error::MmioGuardNotsupported),
-        Err(e) => Err(Error::GeniezoneError(e, VENDOR_HYP_GZVM_MMIO_GUARD_ENROLL_FUNC_ID)),
-    }
-}
-
 fn checked_hvc64_expect_zero(function: u32, args: [u64; 17]) -> Result<()> {
     success_or_error_64(hvc64(function, args)[0]).map_err(|e| Error::GeniezoneError(e, function))
 }
diff --git a/libs/hyp/src/hypervisor/kvm.rs b/libs/hyp/src/hypervisor/kvm.rs
index a95b8de..5835346 100644
--- a/libs/hyp/src/hypervisor/kvm.rs
+++ b/libs/hyp/src/hypervisor/kvm.rs
@@ -14,9 +14,7 @@
 
 //! Wrappers around calls to the KVM hypervisor.
 
-use super::common::{
-    Hypervisor, MemSharingHypervisor, MmioGuardedHypervisor, MMIO_GUARD_GRANULE_SIZE,
-};
+use super::common::{Hypervisor, MemSharingHypervisor, MmioGuardedHypervisor};
 use crate::error::{Error, Result};
 use crate::util::page_address;
 use core::fmt::{self, Display, Formatter};
@@ -95,13 +93,13 @@
 }
 
 impl MmioGuardedHypervisor for ProtectedKvmHypervisor {
-    fn init(&self) -> Result<()> {
-        mmio_guard_enroll()?;
-        let mmio_granule = mmio_guard_granule()?;
-        if mmio_granule != MMIO_GUARD_GRANULE_SIZE {
-            return Err(Error::UnsupportedMmioGuardGranule(mmio_granule));
+    fn enroll(&self) -> Result<()> {
+        let args = [0u64; 17];
+        match success_or_error_64(hvc64(VENDOR_HYP_KVM_MMIO_GUARD_ENROLL_FUNC_ID, args)[0]) {
+            Ok(()) => Ok(()),
+            Err(KvmError::NotSupported) => Err(Error::MmioGuardNotSupported),
+            Err(e) => Err(Error::KvmError(e, VENDOR_HYP_KVM_MMIO_GUARD_ENROLL_FUNC_ID)),
         }
-        Ok(())
     }
 
     fn map(&self, addr: usize) -> Result<()> {
@@ -125,6 +123,12 @@
             Err(e) => Err(Error::KvmError(e, VENDOR_HYP_KVM_MMIO_GUARD_UNMAP_FUNC_ID)),
         }
     }
+
+    fn granule(&self) -> Result<usize> {
+        let args = [0u64; 17];
+        let granule = checked_hvc64(VENDOR_HYP_KVM_MMIO_GUARD_INFO_FUNC_ID, args)?;
+        Ok(granule.try_into().unwrap())
+    }
 }
 
 impl MemSharingHypervisor for ProtectedKvmHypervisor {
@@ -149,22 +153,6 @@
     }
 }
 
-fn mmio_guard_granule() -> Result<usize> {
-    let args = [0u64; 17];
-
-    let granule = checked_hvc64(VENDOR_HYP_KVM_MMIO_GUARD_INFO_FUNC_ID, args)?;
-    Ok(granule.try_into().unwrap())
-}
-
-fn mmio_guard_enroll() -> Result<()> {
-    let args = [0u64; 17];
-    match success_or_error_64(hvc64(VENDOR_HYP_KVM_MMIO_GUARD_ENROLL_FUNC_ID, args)[0]) {
-        Ok(_) => Ok(()),
-        Err(KvmError::NotSupported) => Err(Error::MmioGuardNotsupported),
-        Err(e) => Err(Error::KvmError(e, VENDOR_HYP_KVM_MMIO_GUARD_ENROLL_FUNC_ID)),
-    }
-}
-
 fn checked_hvc64_expect_zero(function: u32, args: [u64; 17]) -> Result<()> {
     success_or_error_64(hvc64(function, args)[0]).map_err(|e| Error::KvmError(e, function))
 }
diff --git a/libs/hyp/src/hypervisor/mod.rs b/libs/hyp/src/hypervisor/mod.rs
index bc9e406..309f967 100644
--- a/libs/hyp/src/hypervisor/mod.rs
+++ b/libs/hyp/src/hypervisor/mod.rs
@@ -60,8 +60,10 @@
             GeniezoneHypervisor::UUID => Ok(HypervisorBackend::Geniezone),
             GunyahHypervisor::UUID => Ok(HypervisorBackend::Gunyah),
             RegularKvmHypervisor::UUID => {
-                // Protected KVM has the same UUID so differentiate based on MEM_SHARE.
-                match ProtectedKvmHypervisor.as_mem_sharer().unwrap().granule() {
+                // Protected KVM has the same UUID as "regular" KVM so issue an HVC that is assumed
+                // to only be supported by pKVM: if it returns SUCCESS, deduce that this is pKVM
+                // and if it returns NOT_SUPPORTED assume that it is "regular" KVM.
+                match ProtectedKvmHypervisor.as_mmio_guard().unwrap().granule() {
                     Ok(_) => Ok(HypervisorBackend::ProtectedKvm),
                     Err(Error::KvmError(KvmError::NotSupported, _)) => {
                         Ok(HypervisorBackend::RegularKvm)
@@ -101,7 +103,7 @@
 }
 
 fn detect_hypervisor() -> HypervisorBackend {
-    query_vendor_hyp_call_uid().try_into().expect("Unknown hypervisor")
+    query_vendor_hyp_call_uid().try_into().expect("Failed to detect hypervisor")
 }
 
 /// Gets the hypervisor singleton.
diff --git a/libs/statslog_virtualization/statslog_wrapper.rs b/libs/statslog_virtualization/statslog_wrapper.rs
index 4d1a0fa..b069d7c 100644
--- a/libs/statslog_virtualization/statslog_wrapper.rs
+++ b/libs/statslog_virtualization/statslog_wrapper.rs
@@ -1,4 +1,5 @@
 #![allow(clippy::too_many_arguments)]
+#![allow(clippy::undocumented_unsafe_blocks)]
 #![allow(missing_docs)]
 #![allow(unused)]
 
diff --git a/microdroid/README.md b/microdroid/README.md
index 5e3f586..dd1505f 100644
--- a/microdroid/README.md
+++ b/microdroid/README.md
@@ -138,73 +138,6 @@
 If you are looking for an example usage of the APIs, you may refer to the [demo
 app](https://android.googlesource.com/platform/packages/modules/Virtualization/+/refs/heads/master/demo/).
 
-## Debuggable microdroid
+## Debugging Microdroid
 
-### Debugging features
-Microdroid supports following debugging features:
-
-- VM log
-- console output
-- kernel output
-- logcat output
-- [ramdump](../docs/debug/ramdump.md)
-- crashdump
-- [adb](#adb)
-- [gdb](#debugging-the-payload-on-microdroid)
-
-### Enabling debugging features
-There's two ways to enable the debugging features:
-
-#### Option 1) Running microdroid on AVF debug policy configured device
-
-microdroid can be started with debugging features by debug policies from the
-host. Host bootloader may provide debug policies to host OS's device tree for
-VMs. Host bootloader MUST NOT provide debug policies for locked devices for
-security reasons.
-
-For protected VM, such device tree will be available in microdroid. microdroid
-can check which debuging features is enabled.
-
-Here are list of device tree properties for debugging features.
-
-- `/avf/guest/common/log`: `<1>` to enable kernel log and logcat. Ignored
-  otherwise.
-- `/avf/guest/common/ramdump`: `<1>` to enable ramdump. Ignored otherwise.
-- `/avf/guest/microdroid/adb`: `<1>` to enable `adb`. Ignored otherwise.
-
-#### Option 2) Lauching microdroid with debug level.
-
-microdroid can be started with debugging features. To do so, first, delete
-`$TEST_ROOT/instance.img`; this is because changing debug settings requires a
-new instance. Then add the `--debug=full` flag to the
-`/apex/com.android.virt/bin/vm run-app` command. This will enable all debugging
-features.
-
-### ADB
-
-If `adb` connection is enabled, launch following command.
-
-```sh
-vm_shell
-```
-
-Done. Now you are logged into Microdroid. Have fun!
-
-Once you have an adb connection with `vm_shell`, `localhost:8000` will be the
-serial of microdroid.
-
-### Debugging the payload on microdroid
-
-Like a normal adb device, you can debug native processes using `lldbclient.py`
-script, either by running a new process, or attaching to an existing process.
-Use `vm_shell` tool above, and then run `lldbclient.py`.
-
-```sh
-adb -s localhost:8000 shell 'mount -o remount,exec /data'
-development/scripts/lldbclient.py -s localhost:8000 --chroot . --user '' \
-    (-p PID | -n NAME | -r ...)
-```
-
-**Note:** We need to pass `--chroot .` to skip verifying device, because
-microdroid doesn't match with the host's lunch target. We need to also pass
-`--user ''` as there is no `su` binary in microdroid.
+Refer to [Debugging protected VMs](../docs/debug/README.md).
diff --git a/pvmfw/README.md b/pvmfw/README.md
index 4e93648..386036d 100644
--- a/pvmfw/README.md
+++ b/pvmfw/README.md
@@ -174,10 +174,12 @@
 blos it refers to. In version 1.0, it describes two blobs:
 
 - entry 0 must point to a valid BCC Handover (see below)
-- entry 1 may point to a [DTBO] to be applied to the pVM device tree
+- entry 1 may point to a [DTBO] to be applied to the pVM device tree. See
+  [debug policy][debug_policy] for an example.
 
 [header]: src/config.rs
 [DTBO]: https://android.googlesource.com/platform/external/dtc/+/refs/heads/master/Documentation/dt-object-internal.txt
+[debug_policy]: ../docs/debug/README.md#debug-policy
 
 #### Virtual Platform Boot Certificate Chain Handover
 
@@ -240,37 +242,6 @@
 [Layering]: https://pigweed.googlesource.com/open-dice/+/refs/heads/main/docs/specification.md#layering-details
 [Trusty-BCC]: https://android.googlesource.com/trusty/lib/+/1696be0a8f3a7103/lib/hwbcc/common/swbcc.c#554
 
-#### pVM Device Tree Overlay
-
-Config header can provide a DTBO to be overlaid on top of the baseline device
-tree from crosvm.
-
-The DTBO may contain debug policies. Debug policies MUST NOT be provided for
-locked devices for security reasons.
-
-Here are an example of DTBO.
-
-```
-/ {
-    fragment@avf {
-        target-path = "/";
-
-        __overlay__ {
-            avf {
-                /* your debug policy here */
-            };
-        };
-    };
-}; /* end of avf */
-```
-
-For specifying DTBO, host bootloader should apply the DTBO to both host
-OS's device tree and config header of `pvmfw`. Both `virtualizationmanager` and
-`pvmfw` will prepare for debugging features.
-
-For details about device tree properties for debug policies, see
-[microdroid's debugging policy guide](../microdroid/README.md#option-1-running-microdroid-on-avf-debug-policy-configured-device).
-
 ### Platform Requirements
 
 pvmfw is intended to run in a virtualized environment according to the `crosvm`
@@ -433,3 +404,25 @@
 kernel][soong-udroid]).
 
 [soong-udroid]: https://cs.android.com/android/platform/superproject/+/master:packages/modules/Virtualization/microdroid/Android.bp;l=427;drc=ca0049be4d84897b8c9956924cfae506773103eb
+
+## Development
+
+For faster iteration, you can build pvmfw, adb-push it to the device, and use
+it directly for a new pVM, without having to flash it to the physical
+partition. To do that, set the system property `hypervisor.pvmfw.path` to point
+to the pvmfw image you pushed as shown below:
+
+```shell
+m pvmfw_img
+adb push out/target/product/generic_arm64/system/etc/pvmfw.img /data/local/tmp/pvmfw.img
+adb root
+adb shell setprop hypervisor.pvmfw.path /data/local/tmp/pvmfw.img
+```
+
+Then run a protected VM, for example:
+
+```shell
+adb shell /apex/com.android.virt/bin/vm run-microdroid --protected
+```
+
+Note: `adb root` is required to set the system property.
diff --git a/pvmfw/avb/src/descriptor/collection.rs b/pvmfw/avb/src/descriptor/collection.rs
index c6698c0..14c47b1 100644
--- a/pvmfw/avb/src/descriptor/collection.rs
+++ b/pvmfw/avb/src/descriptor/collection.rs
@@ -170,9 +170,9 @@
     /// Behavior is undefined if any of the following conditions are violated:
     /// * The `descriptor` pointer must be non-null and point to a valid `AvbDescriptor`.
     unsafe fn from_descriptor_ptr(descriptor: *const AvbDescriptor) -> utils::Result<Self> {
+        let avb_descriptor =
         // SAFETY: It is safe as the raw pointer `descriptor` is non-null and points to
         // a valid `AvbDescriptor`.
-        let avb_descriptor =
             unsafe { get_valid_descriptor(descriptor, avb_descriptor_validate_and_byteswap)? };
         let len = usize_checked_add(
             size_of::<AvbDescriptor>(),
@@ -189,9 +189,9 @@
                 Ok(Self::Hash(descriptor))
             }
             Ok(AvbDescriptorTag::AVB_DESCRIPTOR_TAG_PROPERTY) => {
+                let descriptor =
                 // SAFETY: It is safe because the caller ensures that `descriptor` is a non-null
                 // pointer pointing to a valid struct.
-                let descriptor =
                     unsafe { PropertyDescriptor::from_descriptor_ptr(descriptor, data)? };
                 Ok(Self::Property(descriptor))
             }
diff --git a/pvmfw/avb/src/ops.rs b/pvmfw/avb/src/ops.rs
index e7f0ac7..8f7295c 100644
--- a/pvmfw/avb/src/ops.rs
+++ b/pvmfw/avb/src/ops.rs
@@ -320,8 +320,8 @@
     pub(crate) fn vbmeta_images(&self) -> Result<&[AvbVBMetaData], AvbSlotVerifyError> {
         let data = self.as_ref();
         is_not_null(data.vbmeta_images).map_err(|_| AvbSlotVerifyError::Io)?;
-        // SAFETY: It is safe as the raw pointer `data.vbmeta_images` is a nonnull pointer.
         let vbmeta_images =
+        // SAFETY: It is safe as the raw pointer `data.vbmeta_images` is a nonnull pointer.
             unsafe { slice::from_raw_parts(data.vbmeta_images, data.num_vbmeta_images) };
         Ok(vbmeta_images)
     }
@@ -329,10 +329,10 @@
     pub(crate) fn loaded_partitions(&self) -> Result<&[AvbPartitionData], AvbSlotVerifyError> {
         let data = self.as_ref();
         is_not_null(data.loaded_partitions).map_err(|_| AvbSlotVerifyError::Io)?;
+        let loaded_partitions =
         // SAFETY: It is safe as the raw pointer `data.loaded_partitions` is a nonnull pointer and
         // is guaranteed by libavb to point to a valid `AvbPartitionData` array as part of the
         // `AvbSlotVerifyData` struct.
-        let loaded_partitions =
             unsafe { slice::from_raw_parts(data.loaded_partitions, data.num_loaded_partitions) };
         Ok(loaded_partitions)
     }
diff --git a/pvmfw/platform.dts b/pvmfw/platform.dts
index 74439d9..cb8e30d 100644
--- a/pvmfw/platform.dts
+++ b/pvmfw/platform.dts
@@ -225,6 +225,8 @@
 			0x3000 0x0 0x0 1 &intc 0 0 GIC_SPI (IRQ_BASE + 5) IRQ_TYPE_LEVEL_HIGH
 			0x3800 0x0 0x0 1 &intc 0 0 GIC_SPI (IRQ_BASE + 6) IRQ_TYPE_LEVEL_HIGH
 			0x4000 0x0 0x0 1 &intc 0 0 GIC_SPI (IRQ_BASE + 7) IRQ_TYPE_LEVEL_HIGH
+			0x4800 0x0 0x0 1 &intc 0 0 GIC_SPI (IRQ_BASE + 8) IRQ_TYPE_LEVEL_HIGH
+			0x5000 0x0 0x0 1 &intc 0 0 GIC_SPI (IRQ_BASE + 9) IRQ_TYPE_LEVEL_HIGH
 		>;
 		interrupt-map-mask = <0xf800 0x0 0x0 0x7
 				      0xf800 0x0 0x0 0x7
@@ -233,6 +235,8 @@
 				      0xf800 0x0 0x0 0x7
 				      0xf800 0x0 0x0 0x7
 				      0xf800 0x0 0x0 0x7
+				      0xf800 0x0 0x0 0x7
+				      0xf800 0x0 0x0 0x7
 				      0xf800 0x0 0x0 0x7>;
 	};
 
diff --git a/pvmfw/src/crypto.rs b/pvmfw/src/crypto.rs
index 3d9c8d1..94714c0 100644
--- a/pvmfw/src/crypto.rs
+++ b/pvmfw/src/crypto.rs
@@ -46,17 +46,14 @@
 
 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 mut file = ptr::null();
+        let mut line = 0;
+        // SAFETY: The function writes to the provided pointers, which are valid because they come
+        // from references. It doesn't retain them after it returns.
+        let packed = unsafe { ERR_get_error_line(&mut file, &mut line) };
 
         let packed = packed.try_into().ok()?;
-        // SAFETY - Any non-NULL result is expected to point to a global const C string.
+        // 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 })
@@ -67,16 +64,16 @@
     }
 
     fn library_name(&self) -> Option<&'static CStr> {
-        // SAFETY - Call to a pure function.
+        // 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.
+        // 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.
+        // 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.
+        // SAFETY: Any non-NULL result is expected to point to a global const C string.
         unsafe { as_static_cstr(reason) }
     }
 }
@@ -111,18 +108,18 @@
 
 impl Aead {
     pub fn aes_256_gcm_randnonce() -> Option<&'static Self> {
-        // SAFETY - Returned pointer is checked below.
+        // 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.
+            // 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.
+        // SAFETY: Function should only read from self.
         unsafe { EVP_AEAD_max_overhead(self.as_ref() as *const _) }
     }
 }
@@ -141,7 +138,7 @@
         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.
+        // 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(),
@@ -154,7 +151,7 @@
         };
 
         if result == 1 {
-            // SAFETY - We assume that the non-NULL value points to a valid and static EVP_AEAD.
+            // 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 {})
@@ -162,12 +159,12 @@
     }
 
     pub fn aead(&self) -> Option<&'static Aead> {
-        // SAFETY - The function should only read from self.
+        // 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.
+            // SAFETY: We assume that the non-NULL value points to a valid and static EVP_AEAD.
             Some(unsafe { &*(aead as *const _) })
         }
     }
@@ -178,7 +175,7 @@
         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
+        // 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 {
@@ -197,7 +194,7 @@
         };
 
         if result == 1 {
-            // SAFETY - Any value written to out_len could be a valid usize. The value itself is
+            // 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() };
@@ -213,7 +210,7 @@
         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
+        // 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 {
@@ -232,7 +229,7 @@
         };
 
         if result == 1 {
-            // SAFETY - Any value written to out_len could be a valid usize. The value itself is
+            // 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() };
@@ -272,12 +269,12 @@
 
 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
+    // 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
+    // SAFETY: Only reads from/writes to the provided slices and supports digest was checked not
     // be NULL.
     let result = unsafe {
         HKDF(
@@ -301,6 +298,6 @@
 }
 
 pub fn init() {
-    // SAFETY - Configures the internal state of the library - may be called multiple times.
+    // SAFETY: Configures the internal state of the library - may be called multiple times.
     unsafe { CRYPTO_library_init() }
 }
diff --git a/pvmfw/src/dice.rs b/pvmfw/src/dice.rs
index fbab013..28271d3 100644
--- a/pvmfw/src/dice.rs
+++ b/pvmfw/src/dice.rs
@@ -91,7 +91,7 @@
 /// .data, or provided BCC).
 #[no_mangle]
 unsafe extern "C" fn DiceClearMemory(_ctx: *mut c_void, size: usize, addr: *mut c_void) {
-    // SAFETY - We must trust that the slice will be valid arrays/variables on the C code stack.
+    // SAFETY: We must trust that the slice will be valid arrays/variables on the C code stack.
     let region = unsafe { slice::from_raw_parts_mut(addr as *mut u8, size) };
     flushed_zeroize(region)
 }
diff --git a/pvmfw/src/entry.rs b/pvmfw/src/entry.rs
index f3bd637..9c929a9 100644
--- a/pvmfw/src/entry.rs
+++ b/pvmfw/src/entry.rs
@@ -93,7 +93,7 @@
             RebootReason::InternalError
         })?;
 
-        // SAFETY - The tracker validated the range to be in main memory, mapped, and not overlap.
+        // SAFETY: The tracker validated the range to be in main memory, mapped, and not overlap.
         let fdt = unsafe { slice::from_raw_parts_mut(range.start as *mut u8, range.len()) };
         let fdt = libfdt::Fdt::from_mut_slice(fdt).map_err(|e| {
             error!("Failed to spawn the FDT wrapper: {e}");
@@ -153,9 +153,9 @@
             return Err(RebootReason::InvalidPayload);
         };
 
-        // SAFETY - The tracker validated the range to be in main memory, mapped, and not overlap.
-        let kernel =
-            unsafe { slice::from_raw_parts(kernel_range.start as *const u8, kernel_range.len()) };
+        let kernel = kernel_range.start as *const u8;
+        // SAFETY: The tracker validated the range to be in main memory, mapped, and not overlap.
+        let kernel = unsafe { slice::from_raw_parts(kernel, kernel_range.len()) };
 
         let ramdisk = if let Some(r) = info.initrd_range {
             debug!("Located ramdisk at {r:?}");
@@ -164,7 +164,7 @@
                 RebootReason::InvalidRamdisk
             })?;
 
-            // SAFETY - The region was validated by memory to be in main memory, mapped, and
+            // SAFETY: The region was validated by memory to be in main memory, mapped, and
             // not overlap.
             Some(unsafe { slice::from_raw_parts(r.start as *const u8, r.len()) })
         } else {
@@ -198,7 +198,7 @@
         RebootReason::InternalError
     })?;
 
-    // SAFETY - We only get the appended payload from here, once. The region was statically mapped,
+    // SAFETY: We only get the appended payload from here, once. The region was statically mapped,
     // then remapped by `init_page_table()`.
     let appended_data = unsafe { get_appended_data_slice() };
 
@@ -277,7 +277,7 @@
     // Disable the exception vector, caches and page table and then jump to the payload at the
     // given address, passing it the given FDT pointer.
     //
-    // SAFETY - We're exiting pvmfw by passing the register values we need to a noreturn asm!().
+    // SAFETY: We're exiting pvmfw by passing the register values we need to a noreturn asm!().
     unsafe {
         asm!(
             "cmp {scratch}, {bcc}",
diff --git a/pvmfw/src/fdt.rs b/pvmfw/src/fdt.rs
index 2382b50..244b192 100644
--- a/pvmfw/src/fdt.rs
+++ b/pvmfw/src/fdt.rs
@@ -209,7 +209,7 @@
 impl PciInfo {
     const IRQ_MASK_CELLS: usize = 4;
     const IRQ_MAP_CELLS: usize = 10;
-    const MAX_IRQS: usize = 8;
+    const MAX_IRQS: usize = 10;
 }
 
 type PciAddrRange = AddressRange<(u32, u64), u64, u64>;
@@ -248,14 +248,22 @@
     let range1 = ranges.next().ok_or(FdtError::NotFound)?;
 
     let irq_masks = node.getprop_cells(cstr!("interrupt-map-mask"))?.ok_or(FdtError::NotFound)?;
-    let irq_masks = CellChunkIterator::<{ PciInfo::IRQ_MASK_CELLS }>::new(irq_masks);
-    let irq_masks: ArrayVec<[PciIrqMask; PciInfo::MAX_IRQS]> =
-        irq_masks.take(PciInfo::MAX_IRQS).collect();
+    let mut chunks = CellChunkIterator::<{ PciInfo::IRQ_MASK_CELLS }>::new(irq_masks);
+    let irq_masks = (&mut chunks).take(PciInfo::MAX_IRQS).collect();
+
+    if chunks.next().is_some() {
+        warn!("Input DT has more than {} PCI entries!", PciInfo::MAX_IRQS);
+        return Err(FdtError::NoSpace);
+    }
 
     let irq_maps = node.getprop_cells(cstr!("interrupt-map"))?.ok_or(FdtError::NotFound)?;
-    let irq_maps = CellChunkIterator::<{ PciInfo::IRQ_MAP_CELLS }>::new(irq_maps);
-    let irq_maps: ArrayVec<[PciIrqMap; PciInfo::MAX_IRQS]> =
-        irq_maps.take(PciInfo::MAX_IRQS).collect();
+    let mut chunks = CellChunkIterator::<{ PciInfo::IRQ_MAP_CELLS }>::new(irq_maps);
+    let irq_maps = (&mut chunks).take(PciInfo::MAX_IRQS).collect();
+
+    if chunks.next().is_some() {
+        warn!("Input DT has more than {} PCI entries!", PciInfo::MAX_IRQS);
+        return Err(FdtError::NoSpace);
+    }
 
     Ok(PciInfo { ranges: [range0, range1], irq_masks, irq_maps })
 }
@@ -559,7 +567,7 @@
         *v = v.to_be();
     }
 
-    // SAFETY - array size is the same
+    // SAFETY: array size is the same
     let value = unsafe {
         core::mem::transmute::<
             [u32; NUM_INTERRUPTS * CELLS_PER_INTERRUPT],
@@ -801,7 +809,7 @@
         }
     };
 
-    // SAFETY - on failure, the corrupted DT is restored using the backup.
+    // SAFETY: on failure, the corrupted DT is restored using the backup.
     if let Err(e) = unsafe { fdt.apply_overlay(overlay) } {
         warn!("Failed to apply debug policy: {e}. Recovering...");
         fdt.copy_from_slice(backup_fdt.as_slice())?;
diff --git a/pvmfw/src/gpt.rs b/pvmfw/src/gpt.rs
index 892850c..1060460 100644
--- a/pvmfw/src/gpt.rs
+++ b/pvmfw/src/gpt.rs
@@ -130,7 +130,7 @@
         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
+            // 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 {
diff --git a/virtualizationmanager/src/aidl.rs b/virtualizationmanager/src/aidl.rs
index d0a8e85..b2497b1 100644
--- a/virtualizationmanager/src/aidl.rs
+++ b/virtualizationmanager/src/aidl.rs
@@ -1100,8 +1100,9 @@
         Status::new_service_specific_error_str(-1, Some(format!("Failed to create pipe: {:?}", e)))
     })?;
 
-    // SAFETY: We are the sole owners of these fds as they were just created.
+    // SAFETY: We are the sole owner of this FD as we just created it, and it is valid and open.
     let mut reader = BufReader::new(unsafe { File::from_raw_fd(raw_read_fd) });
+    // SAFETY: We are the sole owner of this FD as we just created it, and it is valid and open.
     let write_fd = unsafe { File::from_raw_fd(raw_write_fd) };
 
     std::thread::spawn(move || loop {
diff --git a/virtualizationmanager/src/atom.rs b/virtualizationmanager/src/atom.rs
index d6eb141..1d2d191 100644
--- a/virtualizationmanager/src/atom.rs
+++ b/virtualizationmanager/src/atom.rs
@@ -83,7 +83,7 @@
 // This matches how crosvm determines the number of logical cores.
 // For telemetry purposes only.
 pub(crate) fn get_num_cpus() -> Option<usize> {
-    // SAFETY - Only integer constants passed back and forth.
+    // SAFETY: Only integer constants passed back and forth.
     let ret = unsafe { libc::sysconf(libc::_SC_NPROCESSORS_CONF) };
     if ret > 0 {
         ret.try_into().ok()
diff --git a/virtualizationmanager/src/crosvm.rs b/virtualizationmanager/src/crosvm.rs
index 8c412f6..31db3f6 100644
--- a/virtualizationmanager/src/crosvm.rs
+++ b/virtualizationmanager/src/crosvm.rs
@@ -592,7 +592,7 @@
     }
 
     let guest_time_ticks = data_list[42].parse::<i64>()?;
-    // SAFETY : It just returns an integer about CPU tick information.
+    // SAFETY: It just returns an integer about CPU tick information.
     let ticks_per_sec = unsafe { sysconf(_SC_CLK_TCK) };
     Ok(guest_time_ticks * MILLIS_PER_SEC / ticks_per_sec)
 }
@@ -910,8 +910,9 @@
 /// Creates a new pipe with the `O_CLOEXEC` flag set, and returns the read side and write side.
 fn create_pipe() -> Result<(File, File), Error> {
     let (raw_read, raw_write) = pipe2(OFlag::O_CLOEXEC)?;
-    // SAFETY: We are the sole owners of these fds as they were just created.
+    // SAFETY: We are the sole owner of this FD as we just created it, and it is valid and open.
     let read_fd = unsafe { File::from_raw_fd(raw_read) };
+    // SAFETY: We are the sole owner of this FD as we just created it, and it is valid and open.
     let write_fd = unsafe { File::from_raw_fd(raw_write) };
     Ok((read_fd, write_fd))
 }
diff --git a/virtualizationmanager/src/debug_config.rs b/virtualizationmanager/src/debug_config.rs
index 7172e7d..9b13475 100644
--- a/virtualizationmanager/src/debug_config.rs
+++ b/virtualizationmanager/src/debug_config.rs
@@ -42,7 +42,7 @@
     }
 
     fn to_path(&self) -> PathBuf {
-        // SAFETY -- unwrap() is safe for to_str() because node_path and prop_name were &str.
+        // unwrap() is safe for to_str() because node_path and prop_name were &str.
         PathBuf::from(
             [
                 "/sys/firmware/devicetree/base",
@@ -129,7 +129,7 @@
                 .map_err(Error::msg)
                 .with_context(|| "Malformed {overlay_file_path:?}")?;
 
-            // SAFETY - Return immediately if error happens. Damaged fdt_buf and fdt are discarded.
+            // SAFETY: Return immediately if error happens. Damaged fdt_buf and fdt are discarded.
             unsafe {
                 fdt.apply_overlay(overlay_fdt).map_err(Error::msg).with_context(|| {
                     "Failed to overlay {overlay_file_path:?} onto empty device tree"
@@ -141,7 +141,7 @@
     }
 
     fn as_fdt(&self) -> &Fdt {
-        // SAFETY - Checked validity of buffer when instantiate.
+        // SAFETY: Checked validity of buffer when instantiate.
         unsafe { Fdt::unchecked_from_slice(&self.buffer) }
     }
 }
diff --git a/virtualizationmanager/src/main.rs b/virtualizationmanager/src/main.rs
index bd7f8af..f058547 100644
--- a/virtualizationmanager/src/main.rs
+++ b/virtualizationmanager/src/main.rs
@@ -86,7 +86,7 @@
     }
     owned_fds.push(raw_fd);
 
-    // SAFETY - Initializing OwnedFd for a RawFd provided in cmdline arguments.
+    // SAFETY: Initializing OwnedFd for a RawFd provided in cmdline arguments.
     // We checked that the integer value corresponds to a valid FD and that this
     // is the first argument to claim its ownership.
     Ok(unsafe { OwnedFd::from_raw_fd(raw_fd) })
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index 5c5a7e4..7dfabb0 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -95,7 +95,7 @@
         let pid = get_calling_pid();
         let lim = libc::rlimit { rlim_cur: libc::RLIM_INFINITY, rlim_max: libc::RLIM_INFINITY };
 
-        // SAFETY - borrowing the new limit struct only
+        // SAFETY: borrowing the new limit struct only
         let ret = unsafe { libc::prlimit(pid, libc::RLIMIT_MEMLOCK, &lim, std::ptr::null_mut()) };
 
         match ret {
diff --git a/vm/src/run.rs b/vm/src/run.rs
index 64da2d9..f50bd50 100644
--- a/vm/src/run.rs
+++ b/vm/src/run.rs
@@ -382,14 +382,14 @@
 /// Safely duplicate the file descriptor.
 fn duplicate_fd<T: AsRawFd>(file: T) -> io::Result<File> {
     let fd = file.as_raw_fd();
-    // Safe because this just duplicates a file descriptor which we know to be valid, and we check
-    // for an error.
+    // SAFETY: This just duplicates a file descriptor which we know to be valid, and we check for an
+    // an error.
     let dup_fd = unsafe { libc::dup(fd) };
     if dup_fd < 0 {
         Err(io::Error::last_os_error())
     } else {
-        // Safe because we have just duplicated the file descriptor so we own it, and `from_raw_fd`
-        // takes ownership of it.
+        // SAFETY: We have just duplicated the file descriptor so we own it, and `from_raw_fd` takes
+        // ownership of it.
         Ok(unsafe { File::from_raw_fd(dup_fd) })
     }
 }
diff --git a/vmbase/Android.bp b/vmbase/Android.bp
index 46f4937..71b9e76 100644
--- a/vmbase/Android.bp
+++ b/vmbase/Android.bp
@@ -84,6 +84,7 @@
         "libspin_nostd",
         "libtinyvec_nostd",
         "libvirtio_drivers",
+        "libzerocopy_nostd",
         "libzeroize_nostd",
     ],
     whole_static_libs: [
diff --git a/vmbase/src/entry.rs b/vmbase/src/entry.rs
index 24b5035..2ff66cc 100644
--- a/vmbase/src/entry.rs
+++ b/vmbase/src/entry.rs
@@ -26,7 +26,8 @@
     console::init();
 
     if let Some(mmio_guard) = get_mmio_guard() {
-        mmio_guard.init()?;
+        mmio_guard.enroll()?;
+        mmio_guard.validate_granule()?;
         mmio_guard.map(console::BASE_ADDRESS)?;
     }
 
diff --git a/vmbase/src/hvc.rs b/vmbase/src/hvc.rs
index ebd1625..1197143 100644
--- a/vmbase/src/hvc.rs
+++ b/vmbase/src/hvc.rs
@@ -37,7 +37,7 @@
     (version as u32 as i32).try_into()
 }
 
-pub type TrngRng64Entropy = (u64, u64, u64);
+pub type TrngRng64Entropy = [u64; 3];
 
 pub fn trng_rnd64(nbits: u64) -> trng::Result<TrngRng64Entropy> {
     let mut args = [0u64; 17];
@@ -46,7 +46,7 @@
     let regs = hvc64(ARM_SMCCC_TRNG_RND64, args);
     success_or_error_64::<Error>(regs[0])?;
 
-    Ok((regs[1], regs[2], regs[3]))
+    Ok([regs[1], regs[2], regs[3]])
 }
 
 pub fn trng_features(fid: u32) -> trng::Result<u64> {
diff --git a/vmbase/src/rand.rs b/vmbase/src/rand.rs
index 6b8d7e0..2acc390 100644
--- a/vmbase/src/rand.rs
+++ b/vmbase/src/rand.rs
@@ -14,10 +14,13 @@
 
 //! Functions and drivers for obtaining true entropy.
 
-use crate::hvc::{self, TrngRng64Entropy};
+use crate::hvc;
 use core::fmt;
 use core::mem::size_of;
 use smccc::{self, Hvc};
+use zerocopy::AsBytes as _;
+
+type Entropy = [u8; size_of::<u64>() * 3];
 
 /// Error type for rand operations.
 pub enum Error {
@@ -95,46 +98,55 @@
 
 /// Fills a slice of bytes with true entropy.
 pub fn fill_with_entropy(s: &mut [u8]) -> Result<()> {
-    const MAX_BYTES_PER_CALL: usize = size_of::<TrngRng64Entropy>();
+    const MAX_BYTES_PER_CALL: usize = size_of::<Entropy>();
 
-    let (aligned, remainder) = s.split_at_mut(s.len() - s.len() % MAX_BYTES_PER_CALL);
-
-    for chunk in aligned.chunks_exact_mut(MAX_BYTES_PER_CALL) {
-        let (r, s, t) = repeat_trng_rnd(chunk.len())?;
-
-        let mut words = chunk.chunks_exact_mut(size_of::<u64>());
-        words.next().unwrap().clone_from_slice(&t.to_ne_bytes());
-        words.next().unwrap().clone_from_slice(&s.to_ne_bytes());
-        words.next().unwrap().clone_from_slice(&r.to_ne_bytes());
-    }
-
-    if !remainder.is_empty() {
-        let mut entropy = [0; MAX_BYTES_PER_CALL];
-        let (r, s, t) = repeat_trng_rnd(remainder.len())?;
-
-        let mut words = entropy.chunks_exact_mut(size_of::<u64>());
-        words.next().unwrap().clone_from_slice(&t.to_ne_bytes());
-        words.next().unwrap().clone_from_slice(&s.to_ne_bytes());
-        words.next().unwrap().clone_from_slice(&r.to_ne_bytes());
-
-        remainder.clone_from_slice(&entropy[..remainder.len()]);
+    for chunk in s.chunks_mut(MAX_BYTES_PER_CALL) {
+        let entropy = repeat_trng_rnd(chunk.len())?;
+        chunk.clone_from_slice(&entropy[..chunk.len()]);
     }
 
     Ok(())
 }
 
-fn repeat_trng_rnd(n_bytes: usize) -> Result<TrngRng64Entropy> {
-    let bits = usize::try_from(u8::BITS).unwrap();
-    let n_bits = (n_bytes * bits).try_into().unwrap();
+/// Returns an array where the first `n_bytes` bytes hold entropy.
+///
+/// The rest of the array should be ignored.
+fn repeat_trng_rnd(n_bytes: usize) -> Result<Entropy> {
     loop {
-        match hvc::trng_rnd64(n_bits) {
-            Ok(entropy) => return Ok(entropy),
-            Err(hvc::trng::Error::NoEntropy) => (),
-            Err(e) => return Err(e.into()),
+        if let Some(entropy) = rnd64(n_bytes)? {
+            return Ok(entropy);
         }
     }
 }
 
+/// Returns an array where the first `n_bytes` bytes hold entropy, if available.
+///
+/// The rest of the array should be ignored.
+fn rnd64(n_bytes: usize) -> Result<Option<Entropy>> {
+    let bits = usize::try_from(u8::BITS).unwrap();
+    let result = hvc::trng_rnd64((n_bytes * bits).try_into().unwrap());
+    let entropy = if matches!(result, Err(hvc::trng::Error::NoEntropy)) {
+        None
+    } else {
+        let r = result?;
+        // From the SMCCC TRNG:
+        //
+        //     A MAX_BITS-bits wide value (Entropy) is returned across X1 to X3.
+        //     The requested conditioned entropy is returned in Entropy[N-1:0].
+        //
+        //             X1     Entropy[191:128]
+        //             X2     Entropy[127:64]
+        //             X3     Entropy[63:0]
+        //
+        //     The bits in Entropy[MAX_BITS-1:N] are 0.
+        let reordered = [r[2].to_le(), r[1].to_le(), r[0].to_le()];
+
+        Some(reordered.as_bytes().try_into().unwrap())
+    };
+
+    Ok(entropy)
+}
+
 /// Generate an array of fixed-size initialized with true-random bytes.
 pub fn random_array<const N: usize>() -> Result<[u8; N]> {
     let mut arr = [0; N];
diff --git a/vmclient/src/lib.rs b/vmclient/src/lib.rs
index cfd015a..7c0383b 100644
--- a/vmclient/src/lib.rs
+++ b/vmclient/src/lib.rs
@@ -67,7 +67,7 @@
     // file descriptors (expected by SharedChild).
     let (raw1, raw2) = pipe2(OFlag::O_CLOEXEC)?;
 
-    // SAFETY - Taking ownership of brand new FDs.
+    // SAFETY: Taking ownership of brand new FDs.
     unsafe { Ok((OwnedFd::from_raw_fd(raw1), OwnedFd::from_raw_fd(raw2))) }
 }
 
@@ -80,7 +80,7 @@
     let (raw1, raw2) =
         socketpair(AddressFamily::Unix, SockType::Stream, None, SockFlag::SOCK_CLOEXEC)?;
 
-    // SAFETY - Taking ownership of brand new FDs.
+    // SAFETY: Taking ownership of brand new FDs.
     unsafe { Ok((OwnedFd::from_raw_fd(raw1), OwnedFd::from_raw_fd(raw2))) }
 }