Merge "Move ignorabletest library to common location and rename." 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/authfs/Android.bp b/authfs/Android.bp
index 2532026..154a1d6 100644
--- a/authfs/Android.bp
+++ b/authfs/Android.bp
@@ -23,7 +23,7 @@
         "liblog_rust",
         "libnix",
         "libopenssl",
-        "libprotobuf_deprecated",
+        "libprotobuf",
         "librpcbinder_rs",
         "libthiserror",
     ],
diff --git a/compos/Android.bp b/compos/Android.bp
index c120b0f..2f6be98 100644
--- a/compos/Android.bp
+++ b/compos/Android.bp
@@ -18,7 +18,7 @@
         "libminijail_rust",
         "libnix",
         "libodsign_proto_rust",
-        "libprotobuf_deprecated",
+        "libprotobuf",
         "libregex",
         "librpcbinder_rs",
         "librustutils",
diff --git a/compos/common/compos_client.rs b/compos/common/compos_client.rs
index 77a1204..a8a176a 100644
--- a/compos/common/compos_client.rs
+++ b/compos/common/compos_client.rs
@@ -140,8 +140,15 @@
         // Let logs go to logcat.
         let (console_fd, log_fd) = (None, None);
         let callback = Box::new(Callback {});
-        let instance = VmInstance::create(service, &config, console_fd, log_fd, Some(callback))
-            .context("Failed to create VM")?;
+        let instance = VmInstance::create(
+            service,
+            &config,
+            console_fd,
+            /*console_in_fd */ None,
+            log_fd,
+            Some(callback),
+        )
+        .context("Failed to create VM")?;
 
         instance.start()?;
 
diff --git a/compos/composd/Android.bp b/compos/composd/Android.bp
index f66de32..b0294dd 100644
--- a/compos/composd/Android.bp
+++ b/compos/composd/Android.bp
@@ -22,7 +22,7 @@
         "liblibc",
         "liblog_rust",
         "libodsign_proto_rust",
-        "libprotobuf_deprecated",
+        "libprotobuf",
         "librustutils",
         "libshared_child",
         "libvmclient",
diff --git a/compos/src/artifact_signer.rs b/compos/src/artifact_signer.rs
index d3843fc..908e438 100644
--- a/compos/src/artifact_signer.rs
+++ b/compos/src/artifact_signer.rs
@@ -63,7 +63,7 @@
     /// with accompanying sigature file.
     pub fn write_info_and_signature(self, info_path: &Path) -> Result<()> {
         let mut info = OdsignInfo::new();
-        info.mut_file_hashes().extend(self.file_digests.into_iter());
+        info.file_hashes.extend(self.file_digests.into_iter());
         let bytes = info.write_to_bytes()?;
 
         let signature = compos_key::sign(&bytes)?;
diff --git a/demo_native/main.cpp b/demo_native/main.cpp
index fa87549..bc42036 100644
--- a/demo_native/main.cpp
+++ b/demo_native/main.cpp
@@ -223,10 +223,11 @@
     std::shared_ptr<IVirtualMachine> vm;
 
     VirtualMachineConfig config = std::move(app_config);
-    ScopedFileDescriptor console_fd(fcntl(fileno(stdout), F_DUPFD_CLOEXEC));
+    ScopedFileDescriptor console_out_fd(fcntl(fileno(stdout), F_DUPFD_CLOEXEC));
+    ScopedFileDescriptor console_in_fd(fcntl(fileno(stdin), F_DUPFD_CLOEXEC));
     ScopedFileDescriptor log_fd(fcntl(fileno(stdout), F_DUPFD_CLOEXEC));
 
-    ScopedAStatus ret = service.createVm(config, console_fd, log_fd, &vm);
+    ScopedAStatus ret = service.createVm(config, console_out_fd, console_in_fd, log_fd, &vm);
     if (!ret.isOk()) {
         return Error() << "Failed to create VM";
     }
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/javalib/api/test-current.txt b/javalib/api/test-current.txt
index 8b7ec11..cf95770 100644
--- a/javalib/api/test-current.txt
+++ b/javalib/api/test-current.txt
@@ -2,15 +2,19 @@
 package android.system.virtualmachine {
 
   public class VirtualMachine implements java.lang.AutoCloseable {
+    method @NonNull @WorkerThread public java.io.OutputStream getConsoleInput() throws android.system.virtualmachine.VirtualMachineException;
     method @NonNull public java.io.File getRootDir();
   }
 
   public final class VirtualMachineConfig {
     method @Nullable public String getPayloadConfigPath();
+    method public boolean isVmConsoleInputSupported();
   }
 
   public static final class VirtualMachineConfig.Builder {
     method @NonNull @RequiresPermission(android.system.virtualmachine.VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION) public android.system.virtualmachine.VirtualMachineConfig.Builder setPayloadConfigPath(@NonNull String);
+    method @NonNull @RequiresPermission(android.system.virtualmachine.VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION) public android.system.virtualmachine.VirtualMachineConfig.Builder setVendorDiskImage(@NonNull java.io.File);
+    method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setVmConsoleInputSupported(boolean);
   }
 
 }
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachine.java b/javalib/src/android/system/virtualmachine/VirtualMachine.java
index f96effa..675a046 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachine.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachine.java
@@ -76,11 +76,13 @@
 
 import java.io.File;
 import java.io.FileInputStream;
+import java.io.FileOutputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.io.OutputStream;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.nio.channels.FileChannel;
@@ -294,6 +296,8 @@
 
     private final boolean mVmOutputCaptured;
 
+    private final boolean mVmConsoleInputSupported;
+
     /** The configuration that is currently associated with this VM. */
     @GuardedBy("mLock")
     @NonNull
@@ -306,11 +310,19 @@
 
     @GuardedBy("mLock")
     @Nullable
-    private ParcelFileDescriptor mConsoleReader;
+    private ParcelFileDescriptor mConsoleOutReader;
 
     @GuardedBy("mLock")
     @Nullable
-    private ParcelFileDescriptor mConsoleWriter;
+    private ParcelFileDescriptor mConsoleOutWriter;
+
+    @GuardedBy("mLock")
+    @Nullable
+    private ParcelFileDescriptor mConsoleInReader;
+
+    @GuardedBy("mLock")
+    @Nullable
+    private ParcelFileDescriptor mConsoleInWriter;
 
     @GuardedBy("mLock")
     @Nullable
@@ -372,6 +384,7 @@
                         : null;
 
         mVmOutputCaptured = config.isVmOutputCaptured();
+        mVmConsoleInputSupported = config.isVmConsoleInputSupported();
     }
 
     /**
@@ -787,7 +800,11 @@
 
             try {
                 if (mVmOutputCaptured) {
-                    createVmPipes();
+                    createVmOutputPipes();
+                }
+
+                if (mVmConsoleInputSupported) {
+                    createVmInputPipes();
                 }
 
                 VirtualMachineAppConfig appConfig =
@@ -804,7 +821,9 @@
                         android.system.virtualizationservice.VirtualMachineConfig.appConfig(
                                 appConfig);
 
-                mVirtualMachine = service.createVm(vmConfigParcel, mConsoleWriter, mLogWriter);
+                mVirtualMachine =
+                        service.createVm(
+                                vmConfigParcel, mConsoleOutWriter, mConsoleInReader, mLogWriter);
                 mVirtualMachine.registerCallback(new CallbackTranslator(service));
                 mContext.registerComponentCallbacks(mMemoryManagementCallbacks);
                 mVirtualMachine.start();
@@ -843,12 +862,12 @@
     }
 
     @GuardedBy("mLock")
-    private void createVmPipes() throws VirtualMachineException {
+    private void createVmOutputPipes() throws VirtualMachineException {
         try {
-            if (mConsoleReader == null || mConsoleWriter == null) {
+            if (mConsoleOutReader == null || mConsoleOutWriter == null) {
                 ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
-                mConsoleReader = pipe[0];
-                mConsoleWriter = pipe[1];
+                mConsoleOutReader = pipe[0];
+                mConsoleOutWriter = pipe[1];
             }
 
             if (mLogReader == null || mLogWriter == null) {
@@ -857,7 +876,20 @@
                 mLogWriter = pipe[1];
             }
         } catch (IOException e) {
-            throw new VirtualMachineException("Failed to create stream for VM", e);
+            throw new VirtualMachineException("Failed to create output stream for VM", e);
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void createVmInputPipes() throws VirtualMachineException {
+        try {
+            if (mConsoleInReader == null || mConsoleInWriter == null) {
+                ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
+                mConsoleInReader = pipe[0];
+                mConsoleInWriter = pipe[1];
+            }
+        } catch (IOException e) {
+            throw new VirtualMachineException("Failed to create input stream for VM", e);
         }
     }
 
@@ -883,12 +915,37 @@
             throw new VirtualMachineException("Capturing vm outputs is turned off");
         }
         synchronized (mLock) {
-            createVmPipes();
-            return new FileInputStream(mConsoleReader.getFileDescriptor());
+            createVmOutputPipes();
+            return new FileInputStream(mConsoleOutReader.getFileDescriptor());
         }
     }
 
     /**
+     * Returns the stream object representing the console input to the virtual machine. The console
+     * input is only available if the {@link VirtualMachineConfig} specifies that it should be
+     * {@linkplain VirtualMachineConfig#isVmConsoleInputSupported supported}.
+     *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
+     * @throws VirtualMachineException if the stream could not be created, or console input is not
+     *     supported.
+     * @hide
+     */
+    @TestApi
+    @WorkerThread
+    @NonNull
+    public OutputStream getConsoleInput() throws VirtualMachineException {
+        if (!mVmConsoleInputSupported) {
+            throw new VirtualMachineException("VM console input is not supported");
+        }
+        synchronized (mLock) {
+            createVmInputPipes();
+            return new FileOutputStream(mConsoleInWriter.getFileDescriptor());
+        }
+    }
+
+
+    /**
      * Returns the stream object representing the log output from the virtual machine. The log
      * output is only available if the VirtualMachineConfig specifies that it should be {@linkplain
      * VirtualMachineConfig#isVmOutputCaptured captured}.
@@ -910,7 +967,7 @@
             throw new VirtualMachineException("Capturing vm outputs is turned off");
         }
         synchronized (mLock) {
-            createVmPipes();
+            createVmOutputPipes();
             return new FileInputStream(mLogReader.getFileDescriptor());
         }
     }
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
index 5f24f5b..4cad2e3 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -75,6 +75,8 @@
     private static final String KEY_CPU_TOPOLOGY = "cpuTopology";
     private static final String KEY_ENCRYPTED_STORAGE_BYTES = "encryptedStorageBytes";
     private static final String KEY_VM_OUTPUT_CAPTURED = "vmOutputCaptured";
+    private static final String KEY_VM_CONSOLE_INPUT_SUPPORTED = "vmConsoleInputSupported";
+    private static final String KEY_VENDOR_DISK_IMAGE_PATH = "vendorDiskImagePath";
 
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
@@ -163,6 +165,11 @@
     /** Whether the app can read console and log output. */
     private final boolean mVmOutputCaptured;
 
+    /** Whether the app can write console input to the VM */
+    private final boolean mVmConsoleInputSupported;
+
+    @Nullable private final File mVendorDiskImage;
+
     private VirtualMachineConfig(
             @Nullable String packageName,
             @Nullable String apkPath,
@@ -173,7 +180,9 @@
             long memoryBytes,
             @CpuTopology int cpuTopology,
             long encryptedStorageBytes,
-            boolean vmOutputCaptured) {
+            boolean vmOutputCaptured,
+            boolean vmConsoleInputSupported,
+            @Nullable File vendorDiskImage) {
         // This is only called from Builder.build(); the builder handles parameter validation.
         mPackageName = packageName;
         mApkPath = apkPath;
@@ -185,6 +194,8 @@
         mCpuTopology = cpuTopology;
         mEncryptedStorageBytes = encryptedStorageBytes;
         mVmOutputCaptured = vmOutputCaptured;
+        mVmConsoleInputSupported = vmConsoleInputSupported;
+        mVendorDiskImage = vendorDiskImage;
     }
 
     /** Loads a config from a file. */
@@ -259,6 +270,12 @@
             builder.setEncryptedStorageBytes(encryptedStorageBytes);
         }
         builder.setVmOutputCaptured(b.getBoolean(KEY_VM_OUTPUT_CAPTURED));
+        builder.setVmConsoleInputSupported(b.getBoolean(KEY_VM_CONSOLE_INPUT_SUPPORTED));
+
+        String vendorDiskImagePath = b.getString(KEY_VENDOR_DISK_IMAGE_PATH);
+        if (vendorDiskImagePath != null) {
+            builder.setVendorDiskImage(new File(vendorDiskImagePath));
+        }
 
         return builder.build();
     }
@@ -294,6 +311,10 @@
             b.putLong(KEY_ENCRYPTED_STORAGE_BYTES, mEncryptedStorageBytes);
         }
         b.putBoolean(KEY_VM_OUTPUT_CAPTURED, mVmOutputCaptured);
+        b.putBoolean(KEY_VM_CONSOLE_INPUT_SUPPORTED, mVmConsoleInputSupported);
+        if (mVendorDiskImage != null) {
+            b.putString(KEY_VENDOR_DISK_IMAGE_PATH, mVendorDiskImage.getAbsolutePath());
+        }
         b.writeToStream(output);
     }
 
@@ -412,6 +433,17 @@
     }
 
     /**
+     * Returns whether the app can write to the VM console.
+     *
+     * @see Builder#setVmConsoleInputSupported
+     * @hide
+     */
+    @TestApi
+    public boolean isVmConsoleInputSupported() {
+        return mVmConsoleInputSupported;
+    }
+
+    /**
      * Tests if this config is compatible with other config. Being compatible means that the configs
      * can be interchangeably used for the same virtual machine; they do not change the VM identity
      * or secrets. Such changes include varying the number of CPUs or the size of the RAM. Changes
@@ -430,6 +462,7 @@
                 && this.mProtectedVm == other.mProtectedVm
                 && this.mEncryptedStorageBytes == other.mEncryptedStorageBytes
                 && this.mVmOutputCaptured == other.mVmOutputCaptured
+                && this.mVmConsoleInputSupported == other.mVmConsoleInputSupported
                 && Objects.equals(this.mPayloadConfigPath, other.mPayloadConfigPath)
                 && Objects.equals(this.mPayloadBinaryName, other.mPayloadBinaryName)
                 && Objects.equals(this.mPackageName, other.mPackageName)
@@ -481,6 +514,20 @@
                 vsConfig.cpuTopology = android.system.virtualizationservice.CpuTopology.ONE_CPU;
                 break;
         }
+        if (mVendorDiskImage != null) {
+            VirtualMachineAppConfig.CustomConfig customConfig =
+                    new VirtualMachineAppConfig.CustomConfig();
+            customConfig.taskProfiles = new String[0];
+            try {
+                customConfig.vendorImage =
+                        ParcelFileDescriptor.open(mVendorDiskImage, MODE_READ_ONLY);
+            } catch (FileNotFoundException e) {
+                throw new VirtualMachineException(
+                        "Failed to open vendor disk image " + mVendorDiskImage.getAbsolutePath(),
+                        e);
+            }
+            vsConfig.customConfig = customConfig;
+        }
         return vsConfig;
     }
 
@@ -551,6 +598,8 @@
         @CpuTopology private int mCpuTopology = CPU_TOPOLOGY_ONE_CPU;
         private long mEncryptedStorageBytes;
         private boolean mVmOutputCaptured = false;
+        private boolean mVmConsoleInputSupported = false;
+        @Nullable private File mVendorDiskImage;
 
         /**
          * Creates a builder for the given context.
@@ -609,6 +658,10 @@
                 throw new IllegalStateException("debug level must be FULL to capture output");
             }
 
+            if (mVmConsoleInputSupported && mDebugLevel != DEBUG_LEVEL_FULL) {
+                throw new IllegalStateException("debug level must be FULL to use console input");
+            }
+
             return new VirtualMachineConfig(
                     packageName,
                     apkPath,
@@ -619,7 +672,9 @@
                     mMemoryBytes,
                     mCpuTopology,
                     mEncryptedStorageBytes,
-                    mVmOutputCaptured);
+                    mVmOutputCaptured,
+                    mVmConsoleInputSupported,
+                    mVendorDiskImage);
         }
 
         /**
@@ -819,5 +874,36 @@
             mVmOutputCaptured = captured;
             return this;
         }
+
+        /**
+         * Sets whether to allow the app to write to the VM console. Default is {@code false}.
+         *
+         * <p>Setting this as {@code true} will allow the app to directly write into {@linkplain
+         * VirtualMachine#getConsoleInput console input}.
+         *
+         * <p>The {@linkplain #setDebugLevel debug level} must be {@link #DEBUG_LEVEL_FULL} to be
+         * set as true.
+         *
+         * @hide
+         */
+        @TestApi
+        @NonNull
+        public Builder setVmConsoleInputSupported(boolean supported) {
+            mVmConsoleInputSupported = supported;
+            return this;
+        }
+
+        /**
+         * Sets the path to the disk image with vendor-specific modules.
+         *
+         * @hide
+         */
+        @TestApi
+        @RequiresPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION)
+        @NonNull
+        public Builder setVendorDiskImage(@NonNull File vendorDiskImage) {
+            mVendorDiskImage = vendorDiskImage;
+            return this;
+        }
     }
 }
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/Android.bp b/libs/hyp/Android.bp
index 1bb8722..8baf9dd 100644
--- a/libs/hyp/Android.bp
+++ b/libs/hyp/Android.bp
@@ -8,7 +8,6 @@
     srcs: ["src/lib.rs"],
     prefer_rlib: true,
     rustlibs: [
-        "libbitflags",
         "libonce_cell_nostd",
         "libsmccc",
         "libuuid_nostd",
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 ec7d168..70fdd0a 100644
--- a/libs/hyp/src/hypervisor/common.rs
+++ b/libs/hyp/src/hypervisor/common.rs
@@ -14,49 +14,62 @@
 
 //! 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;
-use bitflags::bitflags;
 
 /// Expected MMIO guard granule size, validated during MMIO guard initialization.
 pub const MMIO_GUARD_GRANULE_SIZE: usize = SIZE_4KB;
 
-bitflags! {
-    /// Capabilities that Hypervisor backends can declare support for.
-    pub struct HypervisorCap: u32 {
-        /// Capability for guest to share its memory with host at runtime.
-        const DYNAMIC_MEM_SHARE = 0b1;
+/// Trait for the hypervisor.
+pub trait Hypervisor {
+    /// Returns the hypervisor's MMIO_GUARD implementation, if any.
+    fn as_mmio_guard(&self) -> Option<&dyn MmioGuardedHypervisor> {
+        None
+    }
+
+    /// Returns the hypervisor's dynamic memory sharing implementation, if any.
+    fn as_mem_sharer(&self) -> Option<&dyn MemSharingHypervisor> {
+        None
     }
 }
 
-/// Trait for the hypervisor.
-pub trait Hypervisor {
-    /// 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 mmio_guard_init(&self) -> Result<()>;
+pub trait MmioGuardedHypervisor {
+    /// 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.
-    fn mmio_guard_map(&self, addr: usize) -> Result<()>;
+    fn map(&self, addr: usize) -> Result<()>;
 
     /// Unmaps a page containing the given memory address from the hypervisor MMIO guard.
     /// The page size corresponds to the MMIO guard granule size.
-    fn mmio_guard_unmap(&self, addr: usize) -> Result<()>;
+    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 {
     /// Shares a region of memory with host, granting it read, write and execute permissions.
     /// The size of the region is equal to the memory protection granule returned by
     /// [`hyp_meminfo`].
-    fn mem_share(&self, base_ipa: u64) -> Result<()>;
+    fn share(&self, base_ipa: u64) -> Result<()>;
 
     /// Revokes access permission from host to a memory region previously shared with
     /// [`mem_share`]. The size of the region is equal to the memory protection granule returned by
     /// [`hyp_meminfo`].
-    fn mem_unshare(&self, base_ipa: u64) -> Result<()>;
+    fn unshare(&self, base_ipa: u64) -> Result<()>;
 
     /// Returns the memory protection granule size in bytes.
-    fn memory_protection_granule(&self) -> Result<usize>;
-
-    /// Check if required capabilities are supported.
-    fn has_cap(&self, cap: HypervisorCap) -> bool;
+    fn granule(&self) -> Result<usize>;
 }
diff --git a/libs/hyp/src/hypervisor/geniezone.rs b/libs/hyp/src/hypervisor/geniezone.rs
index 0741978..ad18e17 100644
--- a/libs/hyp/src/hypervisor/geniezone.rs
+++ b/libs/hyp/src/hypervisor/geniezone.rs
@@ -14,7 +14,7 @@
 
 //! Wrappers around calls to the GenieZone hypervisor.
 
-use super::common::{Hypervisor, HypervisorCap, 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};
@@ -40,7 +40,6 @@
     // and share the same identification along with guest VMs.
     // The previous uuid was removed due to duplication elsewhere.
     pub const UUID: Uuid = uuid!("7e134ed0-3b82-488d-8cee-69c19211dbe7");
-    const CAPABILITIES: HypervisorCap = HypervisorCap::DYNAMIC_MEM_SHARE;
 }
 
 /// Error from a GenieZone HVC call.
@@ -85,69 +84,68 @@
 }
 
 impl Hypervisor for GeniezoneHypervisor {
-    fn mmio_guard_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));
-        }
-        Ok(())
+    fn as_mmio_guard(&self) -> Option<&dyn MmioGuardedHypervisor> {
+        Some(self)
     }
 
-    fn mmio_guard_map(&self, addr: usize) -> Result<()> {
+    fn as_mem_sharer(&self) -> Option<&dyn MemSharingHypervisor> {
+        Some(self)
+    }
+}
+
+impl MmioGuardedHypervisor for GeniezoneHypervisor {
+    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)),
+        }
+    }
+
+    fn map(&self, addr: usize) -> Result<()> {
         let mut args = [0u64; 17];
         args[0] = page_address(addr);
 
         checked_hvc64_expect_zero(VENDOR_HYP_GZVM_MMIO_GUARD_MAP_FUNC_ID, args)
     }
 
-    fn mmio_guard_unmap(&self, addr: usize) -> Result<()> {
+    fn unmap(&self, addr: usize) -> Result<()> {
         let mut args = [0u64; 17];
         args[0] = page_address(addr);
 
         checked_hvc64_expect_zero(VENDOR_HYP_GZVM_MMIO_GUARD_UNMAP_FUNC_ID, args)
     }
 
-    fn mem_share(&self, base_ipa: u64) -> Result<()> {
+    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 {
+    fn share(&self, base_ipa: u64) -> Result<()> {
         let mut args = [0u64; 17];
         args[0] = base_ipa;
 
         checked_hvc64_expect_zero(ARM_SMCCC_GZVM_FUNC_MEM_SHARE, args)
     }
 
-    fn mem_unshare(&self, base_ipa: u64) -> Result<()> {
+    fn unshare(&self, base_ipa: u64) -> Result<()> {
         let mut args = [0u64; 17];
         args[0] = base_ipa;
 
         checked_hvc64_expect_zero(ARM_SMCCC_GZVM_FUNC_MEM_UNSHARE, args)
     }
 
-    fn memory_protection_granule(&self) -> Result<usize> {
+    fn granule(&self) -> Result<usize> {
         let args = [0u64; 17];
         let granule = checked_hvc64(ARM_SMCCC_GZVM_FUNC_HYP_MEMINFO, args)?;
         Ok(granule.try_into().unwrap())
     }
-
-    fn has_cap(&self, cap: HypervisorCap) -> bool {
-        Self::CAPABILITIES.contains(cap)
-    }
-}
-
-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<()> {
diff --git a/libs/hyp/src/hypervisor/gunyah.rs b/libs/hyp/src/hypervisor/gunyah.rs
index 252430f..45c01bf 100644
--- a/libs/hyp/src/hypervisor/gunyah.rs
+++ b/libs/hyp/src/hypervisor/gunyah.rs
@@ -1,5 +1,4 @@
-use super::common::{Hypervisor, HypervisorCap, MMIO_GUARD_GRANULE_SIZE};
-use crate::error::Result;
+use super::common::Hypervisor;
 use uuid::{uuid, Uuid};
 
 pub(super) struct GunyahHypervisor;
@@ -8,32 +7,4 @@
     pub const UUID: Uuid = uuid!("c1d58fcd-a453-5fdb-9265-ce36673d5f14");
 }
 
-impl Hypervisor for GunyahHypervisor {
-    fn mmio_guard_init(&self) -> Result<()> {
-        Ok(())
-    }
-
-    fn mmio_guard_map(&self, _addr: usize) -> Result<()> {
-        Ok(())
-    }
-
-    fn mmio_guard_unmap(&self, _addr: usize) -> Result<()> {
-        Ok(())
-    }
-
-    fn mem_share(&self, _base_ipa: u64) -> Result<()> {
-        unimplemented!();
-    }
-
-    fn mem_unshare(&self, _base_ipa: u64) -> Result<()> {
-        unimplemented!();
-    }
-
-    fn memory_protection_granule(&self) -> Result<usize> {
-        Ok(MMIO_GUARD_GRANULE_SIZE)
-    }
-
-    fn has_cap(&self, _cap: HypervisorCap) -> bool {
-        false
-    }
-}
+impl Hypervisor for GunyahHypervisor {}
diff --git a/libs/hyp/src/hypervisor/kvm.rs b/libs/hyp/src/hypervisor/kvm.rs
index a89f9b8..5835346 100644
--- a/libs/hyp/src/hypervisor/kvm.rs
+++ b/libs/hyp/src/hypervisor/kvm.rs
@@ -14,7 +14,7 @@
 
 //! Wrappers around calls to the KVM hypervisor.
 
-use super::common::{Hypervisor, HypervisorCap, 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};
@@ -70,26 +70,39 @@
 const VENDOR_HYP_KVM_MMIO_GUARD_MAP_FUNC_ID: u32 = 0xc6000007;
 const VENDOR_HYP_KVM_MMIO_GUARD_UNMAP_FUNC_ID: u32 = 0xc6000008;
 
-pub(super) struct KvmHypervisor;
+pub(super) struct RegularKvmHypervisor;
 
-impl KvmHypervisor {
+impl RegularKvmHypervisor {
     // Based on ARM_SMCCC_VENDOR_HYP_UID_KVM_REG values listed in Linux kernel source:
     // https://github.com/torvalds/linux/blob/master/include/linux/arm-smccc.h
     pub(super) const UUID: Uuid = uuid!("28b46fb6-2ec5-11e9-a9ca-4b564d003a74");
-    const CAPABILITIES: HypervisorCap = HypervisorCap::DYNAMIC_MEM_SHARE;
 }
 
-impl Hypervisor for KvmHypervisor {
-    fn mmio_guard_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));
-        }
-        Ok(())
+impl Hypervisor for RegularKvmHypervisor {}
+
+pub(super) struct ProtectedKvmHypervisor;
+
+impl Hypervisor for ProtectedKvmHypervisor {
+    fn as_mmio_guard(&self) -> Option<&dyn MmioGuardedHypervisor> {
+        Some(self)
     }
 
-    fn mmio_guard_map(&self, addr: usize) -> Result<()> {
+    fn as_mem_sharer(&self) -> Option<&dyn MemSharingHypervisor> {
+        Some(self)
+    }
+}
+
+impl MmioGuardedHypervisor for ProtectedKvmHypervisor {
+    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)),
+        }
+    }
+
+    fn map(&self, addr: usize) -> Result<()> {
         let mut args = [0u64; 17];
         args[0] = page_address(addr);
 
@@ -99,7 +112,7 @@
             .map_err(|e| Error::KvmError(e, VENDOR_HYP_KVM_MMIO_GUARD_MAP_FUNC_ID))
     }
 
-    fn mmio_guard_unmap(&self, addr: usize) -> Result<()> {
+    fn unmap(&self, addr: usize) -> Result<()> {
         let mut args = [0u64; 17];
         args[0] = page_address(addr);
 
@@ -111,45 +124,33 @@
         }
     }
 
-    fn mem_share(&self, base_ipa: u64) -> Result<()> {
+    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 {
+    fn share(&self, base_ipa: u64) -> Result<()> {
         let mut args = [0u64; 17];
         args[0] = base_ipa;
 
         checked_hvc64_expect_zero(ARM_SMCCC_KVM_FUNC_MEM_SHARE, args)
     }
 
-    fn mem_unshare(&self, base_ipa: u64) -> Result<()> {
+    fn unshare(&self, base_ipa: u64) -> Result<()> {
         let mut args = [0u64; 17];
         args[0] = base_ipa;
 
         checked_hvc64_expect_zero(ARM_SMCCC_KVM_FUNC_MEM_UNSHARE, args)
     }
 
-    fn memory_protection_granule(&self) -> Result<usize> {
+    fn granule(&self) -> Result<usize> {
         let args = [0u64; 17];
         let granule = checked_hvc64(ARM_SMCCC_KVM_FUNC_HYP_MEMINFO, args)?;
         Ok(granule.try_into().unwrap())
     }
-
-    fn has_cap(&self, cap: HypervisorCap) -> bool {
-        Self::CAPABILITIES.contains(cap)
-    }
-}
-
-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<()> {
diff --git a/libs/hyp/src/hypervisor/mod.rs b/libs/hyp/src/hypervisor/mod.rs
index 93d53fe..309f967 100644
--- a/libs/hyp/src/hypervisor/mod.rs
+++ b/libs/hyp/src/hypervisor/mod.rs
@@ -23,30 +23,31 @@
 
 use crate::error::{Error, Result};
 use alloc::boxed::Box;
-pub use common::Hypervisor;
-pub use common::HypervisorCap;
-pub use common::MMIO_GUARD_GRANULE_SIZE;
+use common::Hypervisor;
+pub use common::{MemSharingHypervisor, MmioGuardedHypervisor, MMIO_GUARD_GRANULE_SIZE};
 pub use geniezone::GeniezoneError;
 use geniezone::GeniezoneHypervisor;
 use gunyah::GunyahHypervisor;
 pub use kvm::KvmError;
-use kvm::KvmHypervisor;
+use kvm::{ProtectedKvmHypervisor, RegularKvmHypervisor};
 use once_cell::race::OnceBox;
 use smccc::hvc64;
 use uuid::Uuid;
 
 enum HypervisorBackend {
-    Kvm,
+    RegularKvm,
     Gunyah,
     Geniezone,
+    ProtectedKvm,
 }
 
 impl HypervisorBackend {
     fn get_hypervisor(&self) -> &'static dyn Hypervisor {
         match self {
-            Self::Kvm => &KvmHypervisor,
+            Self::RegularKvm => &RegularKvmHypervisor,
             Self::Gunyah => &GunyahHypervisor,
             Self::Geniezone => &GeniezoneHypervisor,
+            Self::ProtectedKvm => &ProtectedKvmHypervisor,
         }
     }
 }
@@ -58,7 +59,18 @@
         match uuid {
             GeniezoneHypervisor::UUID => Ok(HypervisorBackend::Geniezone),
             GunyahHypervisor::UUID => Ok(HypervisorBackend::Gunyah),
-            KvmHypervisor::UUID => Ok(HypervisorBackend::Kvm),
+            RegularKvmHypervisor::UUID => {
+                // 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)
+                    }
+                    Err(e) => Err(e),
+                }
+            }
             u => Err(Error::UnsupportedHypervisorUuid(u)),
         }
     }
@@ -91,12 +103,22 @@
 }
 
 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.
-pub fn get_hypervisor() -> &'static dyn Hypervisor {
+fn get_hypervisor() -> &'static dyn Hypervisor {
     static HYPERVISOR: OnceBox<HypervisorBackend> = OnceBox::new();
 
     HYPERVISOR.get_or_init(|| Box::new(detect_hypervisor())).get_hypervisor()
 }
+
+/// Gets the MMIO_GUARD hypervisor singleton, if any.
+pub fn get_mmio_guard() -> Option<&'static dyn MmioGuardedHypervisor> {
+    get_hypervisor().as_mmio_guard()
+}
+
+/// Gets the dynamic memory sharing hypervisor singleton, if any.
+pub fn get_mem_sharer() -> Option<&'static dyn MemSharingHypervisor> {
+    get_hypervisor().as_mem_sharer()
+}
diff --git a/libs/hyp/src/lib.rs b/libs/hyp/src/lib.rs
index 32a59d1..486a181 100644
--- a/libs/hyp/src/lib.rs
+++ b/libs/hyp/src/lib.rs
@@ -21,6 +21,6 @@
 mod util;
 
 pub use error::{Error, Result};
-pub use hypervisor::{get_hypervisor, Hypervisor, HypervisorCap, KvmError, MMIO_GUARD_GRANULE_SIZE};
+pub use hypervisor::{get_mem_sharer, get_mmio_guard, KvmError, MMIO_GUARD_GRANULE_SIZE};
 
 use hypervisor::GeniezoneError;
diff --git a/libs/libfdt/src/lib.rs b/libs/libfdt/src/lib.rs
index 8e0bb65..afc36d0 100644
--- a/libs/libfdt/src/lib.rs
+++ b/libs/libfdt/src/lib.rs
@@ -16,6 +16,8 @@
 //! to a bare-metal environment.
 
 #![no_std]
+#![deny(unsafe_op_in_unsafe_fn)]
+#![deny(clippy::undocumented_unsafe_blocks)]
 
 mod iterators;
 
@@ -205,7 +207,7 @@
     }
     /// Find parent node.
     pub fn parent(&self) -> Result<Self> {
-        // SAFETY - Accesses (read-only) are constrained to the DT totalsize.
+        // SAFETY: Accesses (read-only) are constrained to the DT totalsize.
         let ret = unsafe { libfdt_bindgen::fdt_parent_offset(self.fdt.as_ptr(), self.offset) };
 
         Ok(Self { fdt: self.fdt, offset: fdt_err(ret)? })
@@ -311,7 +313,7 @@
         name: &CStr,
     ) -> Result<Option<(*const c_void, usize)>> {
         let mut len: i32 = 0;
-        // SAFETY - Accesses are constrained to the DT totalsize (validated by ctor) and the
+        // SAFETY: Accesses are constrained to the DT totalsize (validated by ctor) and the
         // function respects the passed number of characters.
         let prop = unsafe {
             libfdt_bindgen::fdt_getprop_namelen(
@@ -342,7 +344,7 @@
     }
 
     fn next_compatible(self, compatible: &CStr) -> Result<Option<Self>> {
-        // SAFETY - Accesses (read-only) are constrained to the DT totalsize.
+        // SAFETY: Accesses (read-only) are constrained to the DT totalsize.
         let ret = unsafe {
             libfdt_bindgen::fdt_node_offset_by_compatible(
                 self.fdt.as_ptr(),
@@ -355,14 +357,14 @@
     }
 
     fn address_cells(&self) -> Result<AddrCells> {
-        // SAFETY - Accesses are constrained to the DT totalsize (validated by ctor).
+        // SAFETY: Accesses are constrained to the DT totalsize (validated by ctor).
         unsafe { libfdt_bindgen::fdt_address_cells(self.fdt.as_ptr(), self.offset) }
             .try_into()
             .map_err(|_| FdtError::Internal)
     }
 
     fn size_cells(&self) -> Result<SizeCells> {
-        // SAFETY - Accesses are constrained to the DT totalsize (validated by ctor).
+        // SAFETY: Accesses are constrained to the DT totalsize (validated by ctor).
         unsafe { libfdt_bindgen::fdt_size_cells(self.fdt.as_ptr(), self.offset) }
             .try_into()
             .map_err(|_| FdtError::Internal)
@@ -378,7 +380,7 @@
 impl<'a> FdtNodeMut<'a> {
     /// Append a property name-value (possibly empty) pair to the given node.
     pub fn appendprop<T: AsRef<[u8]>>(&mut self, name: &CStr, value: &T) -> Result<()> {
-        // SAFETY - Accesses are constrained to the DT totalsize (validated by ctor).
+        // SAFETY: Accesses are constrained to the DT totalsize (validated by ctor).
         let ret = unsafe {
             libfdt_bindgen::fdt_appendprop(
                 self.fdt.as_mut_ptr(),
@@ -394,7 +396,7 @@
 
     /// Append a (address, size) pair property to the given node.
     pub fn appendprop_addrrange(&mut self, name: &CStr, addr: u64, size: u64) -> Result<()> {
-        // SAFETY - Accesses are constrained to the DT totalsize (validated by ctor).
+        // SAFETY: Accesses are constrained to the DT totalsize (validated by ctor).
         let ret = unsafe {
             libfdt_bindgen::fdt_appendprop_addrrange(
                 self.fdt.as_mut_ptr(),
@@ -411,7 +413,7 @@
 
     /// Create or change a property name-value pair to the given node.
     pub fn setprop(&mut self, name: &CStr, value: &[u8]) -> Result<()> {
-        // SAFETY - New value size is constrained to the DT totalsize
+        // SAFETY: New value size is constrained to the DT totalsize
         //          (validated by underlying libfdt).
         let ret = unsafe {
             libfdt_bindgen::fdt_setprop(
@@ -429,7 +431,7 @@
     /// Replace the value of the given property with the given value, and ensure that the given
     /// value has the same length as the current value length
     pub fn setprop_inplace(&mut self, name: &CStr, value: &[u8]) -> Result<()> {
-        // SAFETY - fdt size is not altered
+        // SAFETY: fdt size is not altered
         let ret = unsafe {
             libfdt_bindgen::fdt_setprop_inplace(
                 self.fdt.as_mut_ptr(),
@@ -457,7 +459,7 @@
 
     /// 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
+        // 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.
@@ -470,7 +472,7 @@
 
     /// Overwrite the given property with FDT_NOP, effectively removing it from the DT.
     pub fn nop_property(&mut self, name: &CStr) -> Result<()> {
-        // SAFETY - Accesses are constrained to the DT totalsize (validated by ctor) when the
+        // SAFETY: Accesses are constrained to the DT totalsize (validated by ctor) when the
         // library locates the node's property.
         let ret = unsafe {
             libfdt_bindgen::fdt_nop_property(self.fdt.as_mut_ptr(), self.offset, name.as_ptr())
@@ -490,7 +492,7 @@
             return Err(FdtError::NoSpace);
         }
 
-        // SAFETY - new_size is smaller than the old size
+        // SAFETY: new_size is smaller than the old size
         let ret = unsafe {
             libfdt_bindgen::fdt_setprop(
                 self.fdt.as_mut_ptr(),
@@ -511,7 +513,7 @@
 
     /// Add a new subnode to the given node and return it as a FdtNodeMut on success.
     pub fn add_subnode(&'a mut self, name: &CStr) -> Result<Self> {
-        // SAFETY - Accesses are constrained to the DT totalsize (validated by ctor).
+        // SAFETY: Accesses are constrained to the DT totalsize (validated by ctor).
         let ret = unsafe {
             libfdt_bindgen::fdt_add_subnode(self.fdt.as_mut_ptr(), self.offset, name.as_ptr())
         };
@@ -520,7 +522,7 @@
     }
 
     fn parent(&'a self) -> Result<FdtNode<'a>> {
-        // SAFETY - Accesses (read-only) are constrained to the DT totalsize.
+        // SAFETY: Accesses (read-only) are constrained to the DT totalsize.
         let ret = unsafe { libfdt_bindgen::fdt_parent_offset(self.fdt.as_ptr(), self.offset) };
 
         Ok(FdtNode { fdt: &*self.fdt, offset: fdt_err(ret)? })
@@ -528,7 +530,7 @@
 
     /// Return the compatible node of the given name that is next to this node
     pub fn next_compatible(self, compatible: &CStr) -> Result<Option<Self>> {
-        // SAFETY - Accesses (read-only) are constrained to the DT totalsize.
+        // SAFETY: Accesses (read-only) are constrained to the DT totalsize.
         let ret = unsafe {
             libfdt_bindgen::fdt_node_offset_by_compatible(
                 self.fdt.as_ptr(),
@@ -553,7 +555,7 @@
     // mutable reference to DT, so we can't use current node (which also has a mutable reference to
     // DT).
     pub fn delete_and_next_compatible(self, compatible: &CStr) -> Result<Option<Self>> {
-        // SAFETY - Accesses (read-only) are constrained to the DT totalsize.
+        // SAFETY: Accesses (read-only) are constrained to the DT totalsize.
         let ret = unsafe {
             libfdt_bindgen::fdt_node_offset_by_compatible(
                 self.fdt.as_ptr(),
@@ -563,7 +565,7 @@
         };
         let next_offset = fdt_err_or_option(ret)?;
 
-        // SAFETY - fdt_nop_node alter only the bytes in the blob which contain the node and its
+        // SAFETY: fdt_nop_node alter only the bytes in the blob which contain the node and its
         // properties and subnodes, and will not alter or move any other part of the tree.
         let ret = unsafe { libfdt_bindgen::fdt_nop_node(self.fdt.as_mut_ptr(), self.offset) };
         fdt_err_expect_zero(ret)?;
@@ -611,7 +613,7 @@
     ///
     /// Fails if the FDT does not pass validation.
     pub fn from_slice(fdt: &[u8]) -> Result<&Self> {
-        // SAFETY - The FDT will be validated before it is returned.
+        // SAFETY: The FDT will be validated before it is returned.
         let fdt = unsafe { Self::unchecked_from_slice(fdt) };
         fdt.check_full()?;
         Ok(fdt)
@@ -621,7 +623,7 @@
     ///
     /// Fails if the FDT does not pass validation.
     pub fn from_mut_slice(fdt: &mut [u8]) -> Result<&mut Self> {
-        // SAFETY - The FDT will be validated before it is returned.
+        // SAFETY: The FDT will be validated before it is returned.
         let fdt = unsafe { Self::unchecked_from_mut_slice(fdt) };
         fdt.check_full()?;
         Ok(fdt)
@@ -629,7 +631,7 @@
 
     /// Creates an empty Flattened Device Tree with a mutable slice.
     pub fn create_empty_tree(fdt: &mut [u8]) -> Result<&mut Self> {
-        // SAFETY - fdt_create_empty_tree() only write within the specified length,
+        // SAFETY: fdt_create_empty_tree() only write within the specified length,
         //          and returns error if buffer was insufficient.
         //          There will be no memory write outside of the given fdt.
         let ret = unsafe {
@@ -640,7 +642,7 @@
         };
         fdt_err_expect_zero(ret)?;
 
-        // SAFETY - The FDT will be validated before it is returned.
+        // SAFETY: The FDT will be validated before it is returned.
         let fdt = unsafe { Self::unchecked_from_mut_slice(fdt) };
         fdt.check_full()?;
 
@@ -653,7 +655,9 @@
     ///
     /// The returned FDT might be invalid, only use on slices containing a valid DT.
     pub unsafe fn unchecked_from_slice(fdt: &[u8]) -> &Self {
-        mem::transmute::<&[u8], &Self>(fdt)
+        // SAFETY: Fdt is a wrapper around a [u8], so the transmute is valid. The caller is
+        // responsible for ensuring that it is actually a valid FDT.
+        unsafe { mem::transmute::<&[u8], &Self>(fdt) }
     }
 
     /// Wraps a mutable slice containing a Flattened Device Tree.
@@ -662,7 +666,9 @@
     ///
     /// The returned FDT might be invalid, only use on slices containing a valid DT.
     pub unsafe fn unchecked_from_mut_slice(fdt: &mut [u8]) -> &mut Self {
-        mem::transmute::<&mut [u8], &mut Self>(fdt)
+        // SAFETY: Fdt is a wrapper around a [u8], so the transmute is valid. The caller is
+        // responsible for ensuring that it is actually a valid FDT.
+        unsafe { mem::transmute::<&mut [u8], &mut Self>(fdt) }
     }
 
     /// Update this FDT from a slice containing another FDT
@@ -682,7 +688,7 @@
 
     /// Make the whole slice containing the DT available to libfdt.
     pub fn unpack(&mut self) -> Result<()> {
-        // SAFETY - "Opens" the DT in-place (supported use-case) by updating its header and
+        // SAFETY: "Opens" the DT in-place (supported use-case) by updating its header and
         // internal structures to make use of the whole self.fdt slice but performs no accesses
         // outside of it and leaves the DT in a state that will be detected by other functions.
         let ret = unsafe {
@@ -699,7 +705,7 @@
     ///
     /// Doesn't shrink the underlying memory slice.
     pub fn pack(&mut self) -> Result<()> {
-        // SAFETY - "Closes" the DT in-place by updating its header and relocating its structs.
+        // SAFETY: "Closes" the DT in-place by updating its header and relocating its structs.
         let ret = unsafe { libfdt_bindgen::fdt_pack(self.as_mut_ptr()) };
         fdt_err_expect_zero(ret)
     }
@@ -710,10 +716,12 @@
     ///
     /// On failure, the library corrupts the DT and overlay so both must be discarded.
     pub unsafe fn apply_overlay<'a>(&'a mut self, overlay: &'a mut Fdt) -> Result<&'a mut Self> {
-        fdt_err_expect_zero(libfdt_bindgen::fdt_overlay_apply(
-            self.as_mut_ptr(),
-            overlay.as_mut_ptr(),
-        ))?;
+        let ret =
+        // SAFETY: Both pointers are valid because they come from references, and fdt_overlay_apply
+        // doesn't keep them after it returns. It may corrupt their contents if there is an error,
+        // but that's our caller's responsibility.
+            unsafe { libfdt_bindgen::fdt_overlay_apply(self.as_mut_ptr(), overlay.as_mut_ptr()) };
+        fdt_err_expect_zero(ret)?;
         Ok(self)
     }
 
@@ -779,7 +787,7 @@
 
     fn path_offset(&self, path: &CStr) -> Result<Option<c_int>> {
         let len = path.to_bytes().len().try_into().map_err(|_| FdtError::BadPath)?;
-        // SAFETY - Accesses are constrained to the DT totalsize (validated by ctor) and the
+        // SAFETY: Accesses are constrained to the DT totalsize (validated by ctor) and the
         // function respects the passed number of characters.
         let ret = unsafe {
             // *_namelen functions don't include the trailing nul terminator in 'len'.
@@ -791,7 +799,7 @@
 
     fn check_full(&self) -> Result<()> {
         let len = self.buffer.len();
-        // SAFETY - Only performs read accesses within the limits of the slice. If successful, this
+        // SAFETY: Only performs read accesses within the limits of the slice. If successful, this
         // call guarantees to other unsafe calls that the header contains a valid totalsize (w.r.t.
         // 'len' i.e. the self.fdt slice) that those C functions can use to perform bounds
         // checking. The library doesn't maintain an internal state (such as pointers) between
@@ -815,7 +823,7 @@
 
     fn header(&self) -> &libfdt_bindgen::fdt_header {
         let p = self.as_ptr().cast::<_>();
-        // SAFETY - A valid FDT (verified by constructor) must contain a valid fdt_header.
+        // SAFETY: A valid FDT (verified by constructor) must contain a valid fdt_header.
         unsafe { &*p }
     }
 
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/Android.bp b/microdroid/Android.bp
index 5440695..2d3f084 100644
--- a/microdroid/Android.bp
+++ b/microdroid/Android.bp
@@ -23,6 +23,10 @@
     "apex",
     "linkerconfig",
     "second_stage_resources",
+
+    // Ideally we should only create the /vendor for Microdroid VMs that will mount /vendor, but
+    // for the time being we will just create it unconditionally.
+    "vendor",
 ]
 
 microdroid_symlinks = [
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/microdroid/bootconfig.x86_64 b/microdroid/bootconfig.x86_64
index 6076889..eed9212 100644
--- a/microdroid/bootconfig.x86_64
+++ b/microdroid/bootconfig.x86_64
@@ -1 +1 @@
-androidboot.boot_devices = pci0000:00/0000:00:04.0,pci0000:00/0000:00:05.0,pci0000:00/0000:00:06.0
+androidboot.boot_devices = pci0000:00/0000:00:04.0,pci0000:00/0000:00:05.0,pci0000:00/0000:00:06.0,pci0000:00/0000:00:07.0
diff --git a/microdroid/fstab.microdroid b/microdroid/fstab.microdroid
index 9478c7c..da000b9 100644
--- a/microdroid/fstab.microdroid
+++ b/microdroid/fstab.microdroid
@@ -1 +1,7 @@
 system /system ext4 noatime,ro,errors=panic wait,slotselect,avb=vbmeta,first_stage_mount,logical
+# This is a temporary solution to unblock other devs that depend on /vendor partition in Microdroid
+# The /vendor partition will only be mounted if the kernel cmdline contains
+# androidboot.microdroid.mount_vendor=1.
+# TODO(b/285855430): this should probably be defined in the DT
+# TODO(b/285855436): should be mounted on top of dm-verity device
+/dev/block/by-name/microdroid-vendor /vendor ext4 noatime,ro,errors=panic wait,first_stage_mount
diff --git a/microdroid/payload/Android.bp b/microdroid/payload/Android.bp
index 4814a64..8225875 100644
--- a/microdroid/payload/Android.bp
+++ b/microdroid/payload/Android.bp
@@ -31,6 +31,7 @@
     protos: ["metadata.proto"],
     source_stem: "microdroid_metadata",
     host_supported: true,
+    use_protobuf3: true,
     apex_available: [
         "com.android.virt",
     ],
diff --git a/microdroid/payload/metadata/Android.bp b/microdroid/payload/metadata/Android.bp
index e3138e8..cd182fc 100644
--- a/microdroid/payload/metadata/Android.bp
+++ b/microdroid/payload/metadata/Android.bp
@@ -12,7 +12,7 @@
     rustlibs: [
         "libanyhow",
         "libmicrodroid_metadata_proto_rust",
-        "libprotobuf_deprecated",
+        "libprotobuf",
     ],
     apex_available: [
         "com.android.virt",
diff --git a/microdroid/payload/metadata/src/lib.rs b/microdroid/payload/metadata/src/lib.rs
index bfbec60..f00391a 100644
--- a/microdroid/payload/metadata/src/lib.rs
+++ b/microdroid/payload/metadata/src/lib.rs
@@ -24,7 +24,7 @@
 use std::io::Write;
 
 pub use microdroid_metadata::metadata::{
-    ApexPayload, ApkPayload, Metadata, Metadata_oneof_payload as PayloadMetadata, PayloadConfig,
+    metadata::Payload as PayloadMetadata, ApexPayload, ApkPayload, Metadata, PayloadConfig,
 };
 
 /// Reads a metadata from a reader
diff --git a/microdroid_manager/src/dice.rs b/microdroid_manager/src/dice.rs
index 3a2a1e6..bacefcd 100644
--- a/microdroid_manager/src/dice.rs
+++ b/microdroid_manager/src/dice.rs
@@ -170,21 +170,23 @@
 /// PayloadConfig = {
 ///   1: tstr // payload_binary_name
 /// }
-pub fn format_payload_config_descriptor(payload_metadata: &PayloadMetadata) -> Result<Vec<u8>> {
+pub fn format_payload_config_descriptor(payload: &PayloadMetadata) -> Result<Vec<u8>> {
     const MICRODROID_PAYLOAD_COMPONENT_NAME: &str = "Microdroid payload";
 
-    let config_descriptor_cbor_value = match payload_metadata {
-        PayloadMetadata::config_path(payload_config_path) => cbor!({
+    let config_descriptor_cbor_value = match payload {
+        PayloadMetadata::ConfigPath(payload_config_path) => cbor!({
             -70002 => MICRODROID_PAYLOAD_COMPONENT_NAME,
             -71000 => payload_config_path
         }),
-        PayloadMetadata::config(payload_config) => cbor!({
+        PayloadMetadata::Config(payload_config) => cbor!({
             -70002 => MICRODROID_PAYLOAD_COMPONENT_NAME,
             -71001 => {1 => payload_config.payload_binary_name}
         }),
+        _ => bail!("Failed to match the payload against a config type: {:?}", payload),
     }
     .context("Failed to build a CBOR Value from payload metadata")?;
     let mut config_descriptor = Vec::new();
+
     ser::into_writer(&config_descriptor_cbor_value, &mut config_descriptor)?;
     Ok(config_descriptor)
 }
@@ -196,7 +198,7 @@
 
     #[test]
     fn payload_metadata_with_path_formats_correctly() -> Result<()> {
-        let payload_metadata = PayloadMetadata::config_path("/config_path".to_string());
+        let payload_metadata = PayloadMetadata::ConfigPath("/config_path".to_string());
         let config_descriptor = format_payload_config_descriptor(&payload_metadata)?;
         static EXPECTED_CONFIG_DESCRIPTOR: &[u8] = &[
             0xa2, 0x3a, 0x00, 0x01, 0x11, 0x71, 0x72, 0x4d, 0x69, 0x63, 0x72, 0x6f, 0x64, 0x72,
@@ -214,7 +216,7 @@
             payload_binary_name: "payload_binary".to_string(),
             ..Default::default()
         };
-        let payload_metadata = PayloadMetadata::config(payload_config);
+        let payload_metadata = PayloadMetadata::Config(payload_config);
         let config_descriptor = format_payload_config_descriptor(&payload_metadata)?;
         static EXPECTED_CONFIG_DESCRIPTOR: &[u8] = &[
             0xa2, 0x3a, 0x00, 0x01, 0x11, 0x71, 0x72, 0x4d, 0x69, 0x63, 0x72, 0x6f, 0x64, 0x72,
diff --git a/microdroid_manager/src/main.rs b/microdroid_manager/src/main.rs
index 9c19feb..1cdcde1 100644
--- a/microdroid_manager/src/main.rs
+++ b/microdroid_manager/src/main.rs
@@ -228,7 +228,7 @@
 
     load_crashkernel_if_supported().context("Failed to load crashkernel")?;
 
-    swap::init_swap().context("Failed to initialise swap")?;
+    swap::init_swap().context("Failed to initialize swap")?;
     info!("swap enabled.");
 
     let service = get_vms_rpc_binder()
@@ -435,8 +435,9 @@
     // Restricted APIs are only allowed to be used by platform or test components. Infer this from
     // the use of a VM config file since those can only be used by platform and test components.
     let allow_restricted_apis = match payload_metadata {
-        PayloadMetadata::config_path(_) => true,
-        PayloadMetadata::config(_) => false,
+        PayloadMetadata::ConfigPath(_) => true,
+        PayloadMetadata::Config(_) => false,
+        _ => false, // default is false for safety
     };
 
     let config = load_config(payload_metadata).context("Failed to load payload metadata")?;
@@ -792,14 +793,14 @@
 
 fn load_config(payload_metadata: PayloadMetadata) -> Result<VmPayloadConfig> {
     match payload_metadata {
-        PayloadMetadata::config_path(path) => {
+        PayloadMetadata::ConfigPath(path) => {
             let path = Path::new(&path);
             info!("loading config from {:?}...", path);
             let file = ioutil::wait_for_file(path, WAIT_TIMEOUT)
                 .with_context(|| format!("Failed to read {:?}", path))?;
             Ok(serde_json::from_reader(file)?)
         }
-        PayloadMetadata::config(payload_config) => {
+        PayloadMetadata::Config(payload_config) => {
             let task = Task {
                 type_: TaskType::MicrodroidLauncher,
                 command: payload_config.payload_binary_name,
@@ -814,6 +815,7 @@
                 enable_authfs: false,
             })
         }
+        _ => bail!("Failed to match config against a config type."),
     }
 }
 
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 6f96fc0..9c929a9 100644
--- a/pvmfw/src/entry.rs
+++ b/pvmfw/src/entry.rs
@@ -23,7 +23,7 @@
 use core::num::NonZeroUsize;
 use core::ops::Range;
 use core::slice;
-use hyp::{get_hypervisor, HypervisorCap};
+use hyp::{get_mem_sharer, get_mmio_guard};
 use log::debug;
 use log::error;
 use log::info;
@@ -33,10 +33,9 @@
 use vmbase::{
     configure_heap, console,
     layout::{self, crosvm},
-    logger, main,
+    main,
     memory::{min_dcache_line_size, MemoryTracker, MEMORY, SIZE_128KB, SIZE_4KB},
     power::reboot,
-    rand,
 };
 use zeroize::Zeroize;
 
@@ -85,17 +84,16 @@
 
 impl<'a> MemorySlices<'a> {
     fn new(fdt: usize, kernel: usize, kernel_size: usize) -> Result<Self, RebootReason> {
-        // SAFETY - SIZE_2MB is non-zero.
-        const FDT_SIZE: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(crosvm::FDT_MAX_SIZE) };
+        let fdt_size = NonZeroUsize::new(crosvm::FDT_MAX_SIZE).unwrap();
         // TODO - Only map the FDT as read-only, until we modify it right before jump_to_payload()
         // e.g. by generating a DTBO for a template DT in main() and, on return, re-map DT as RW,
         // overwrite with the template DT and apply the DTBO.
-        let range = MEMORY.lock().as_mut().unwrap().alloc_mut(fdt, FDT_SIZE).map_err(|e| {
+        let range = MEMORY.lock().as_mut().unwrap().alloc_mut(fdt, fdt_size).map_err(|e| {
             error!("Failed to allocate the FDT range: {e}");
             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}");
@@ -112,8 +110,8 @@
             RebootReason::InvalidFdt
         })?;
 
-        if get_hypervisor().has_cap(HypervisorCap::DYNAMIC_MEM_SHARE) {
-            let granule = get_hypervisor().memory_protection_granule().map_err(|e| {
+        if let Some(mem_sharer) = get_mem_sharer() {
+            let granule = mem_sharer.granule().map_err(|e| {
                 error!("Failed to get memory protection granule: {e}");
                 RebootReason::InternalError
             })?;
@@ -155,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:?}");
@@ -166,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 {
@@ -192,21 +190,7 @@
     // - only perform logging once the logger has been initialized
     // - only access non-pvmfw memory once (and while) it has been mapped
 
-    logger::init(LevelFilter::Info).map_err(|_| RebootReason::InternalError)?;
-
-    // Use debug!() to avoid printing to the UART if we failed to configure it as only local
-    // builds that have tweaked the logger::init() call will actually attempt to log the message.
-
-    get_hypervisor().mmio_guard_init().map_err(|e| {
-        debug!("{e}");
-        RebootReason::InternalError
-    })?;
-
-    get_hypervisor().mmio_guard_map(console::BASE_ADDRESS).map_err(|e| {
-        debug!("Failed to configure the UART: {e}");
-        RebootReason::InternalError
-    })?;
-
+    log::set_max_level(LevelFilter::Info);
     crypto::init();
 
     let page_table = memory::init_page_table().map_err(|e| {
@@ -214,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() };
 
@@ -235,11 +219,6 @@
 
     let slices = MemorySlices::new(fdt, payload, payload_size)?;
 
-    rand::init().map_err(|e| {
-        error!("Failed to initialize rand: {e}");
-        RebootReason::InternalError
-    })?;
-
     // This wrapper allows main() to be blissfully ignorant of platform details.
     let next_bcc = crate::main(slices.fdt, slices.kernel, slices.ramdisk, bcc_slice, debug_policy)?;
 
@@ -253,10 +232,12 @@
     })?;
     // Call unshare_all_memory here (instead of relying on the dtor) while UART is still mapped.
     MEMORY.lock().as_mut().unwrap().unshare_all_memory();
-    get_hypervisor().mmio_guard_unmap(console::BASE_ADDRESS).map_err(|e| {
-        error!("Failed to unshare the UART: {e}");
-        RebootReason::InternalError
-    })?;
+    if let Some(mmio_guard) = get_mmio_guard() {
+        mmio_guard.unmap(console::BASE_ADDRESS).map_err(|e| {
+            error!("Failed to unshare the UART: {e}");
+            RebootReason::InternalError
+        })?;
+    }
 
     // Drop MemoryTracker and deactivate page table.
     drop(MEMORY.lock().take());
@@ -278,25 +259,25 @@
 
     let scratch = layout::scratch_range();
 
-    assert_ne!(scratch.len(), 0, "scratch memory is empty.");
-    assert_eq!(scratch.start % ASM_STP_ALIGN, 0, "scratch memory is misaligned.");
-    assert_eq!(scratch.end % ASM_STP_ALIGN, 0, "scratch memory is misaligned.");
+    assert_ne!(scratch.end - scratch.start, 0, "scratch memory is empty.");
+    assert_eq!(scratch.start.0 % ASM_STP_ALIGN, 0, "scratch memory is misaligned.");
+    assert_eq!(scratch.end.0 % ASM_STP_ALIGN, 0, "scratch memory is misaligned.");
 
-    assert!(bcc.is_within(&scratch));
+    assert!(bcc.is_within(&(scratch.start.0..scratch.end.0)));
     assert_eq!(bcc.start % ASM_STP_ALIGN, 0, "Misaligned guest BCC.");
     assert_eq!(bcc.end % ASM_STP_ALIGN, 0, "Misaligned guest BCC.");
 
     let stack = memory::stack_range();
 
-    assert_ne!(stack.len(), 0, "stack region is empty.");
-    assert_eq!(stack.start % ASM_STP_ALIGN, 0, "Misaligned stack region.");
-    assert_eq!(stack.end % ASM_STP_ALIGN, 0, "Misaligned stack region.");
+    assert_ne!(stack.end - stack.start, 0, "stack region is empty.");
+    assert_eq!(stack.start.0 % ASM_STP_ALIGN, 0, "Misaligned stack region.");
+    assert_eq!(stack.end.0 % ASM_STP_ALIGN, 0, "Misaligned stack region.");
 
     // Zero all memory that could hold secrets and that can't be safely written to from Rust.
     // 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}",
@@ -375,11 +356,11 @@
             sctlr_el1_val = in(reg) SCTLR_EL1_VAL,
             bcc = in(reg) u64::try_from(bcc.start).unwrap(),
             bcc_end = in(reg) u64::try_from(bcc.end).unwrap(),
-            cache_line = in(reg) u64::try_from(scratch.start).unwrap(),
-            scratch = in(reg) u64::try_from(scratch.start).unwrap(),
-            scratch_end = in(reg) u64::try_from(scratch.end).unwrap(),
-            stack = in(reg) u64::try_from(stack.start).unwrap(),
-            stack_end = in(reg) u64::try_from(stack.end).unwrap(),
+            cache_line = in(reg) u64::try_from(scratch.start.0).unwrap(),
+            scratch = in(reg) u64::try_from(scratch.start.0).unwrap(),
+            scratch_end = in(reg) u64::try_from(scratch.end.0).unwrap(),
+            stack = in(reg) u64::try_from(stack.start.0).unwrap(),
+            stack_end = in(reg) u64::try_from(stack.end.0).unwrap(),
             dcache_line_size = in(reg) u64::try_from(min_dcache_line_size()).unwrap(),
             in("x0") fdt_address,
             in("x30") payload_start,
@@ -396,7 +377,7 @@
     let range = memory::appended_payload_range();
     // SAFETY: This region is mapped and the linker script prevents it from overlapping with other
     // objects.
-    unsafe { slice::from_raw_parts_mut(range.start as *mut u8, range.len()) }
+    unsafe { slice::from_raw_parts_mut(range.start.0 as *mut u8, range.end - range.start) }
 }
 
 enum AppendedConfigType {
diff --git a/pvmfw/src/exceptions.rs b/pvmfw/src/exceptions.rs
index c3f8a29..d9f0891 100644
--- a/pvmfw/src/exceptions.rs
+++ b/pvmfw/src/exceptions.rs
@@ -14,125 +14,34 @@
 
 //! Exception handlers.
 
-use core::fmt;
-use vmbase::console;
-use vmbase::logger;
-use vmbase::memory::{page_4kb_of, MemoryTrackerError, MEMORY};
-use vmbase::read_sysreg;
-use vmbase::{eprintln, power::reboot};
+use vmbase::{
+    eprintln,
+    exceptions::{ArmException, Esr, HandleExceptionError},
+    logger,
+    memory::{handle_permission_fault, handle_translation_fault},
+    power::reboot,
+    read_sysreg,
+};
 
-const UART_PAGE: usize = page_4kb_of(console::BASE_ADDRESS);
-
-#[derive(Debug)]
-enum HandleExceptionError {
-    PageTableUnavailable,
-    PageTableNotInitialized,
-    InternalError(MemoryTrackerError),
-    UnknownException,
-}
-
-impl From<MemoryTrackerError> for HandleExceptionError {
-    fn from(other: MemoryTrackerError) -> Self {
-        Self::InternalError(other)
-    }
-}
-
-impl fmt::Display for HandleExceptionError {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        match self {
-            Self::PageTableUnavailable => write!(f, "Page table is not available."),
-            Self::PageTableNotInitialized => write!(f, "Page table is not initialized."),
-            Self::InternalError(e) => write!(f, "Error while updating page table: {e}"),
-            Self::UnknownException => write!(f, "An unknown exception occurred, not handled."),
-        }
-    }
-}
-
-#[derive(Debug, PartialEq, Copy, Clone)]
-enum Esr {
-    DataAbortTranslationFault,
-    DataAbortPermissionFault,
-    DataAbortSyncExternalAbort,
-    Unknown(usize),
-}
-
-impl Esr {
-    const EXT_DABT_32BIT: usize = 0x96000010;
-    const TRANSL_FAULT_BASE_32BIT: usize = 0x96000004;
-    const TRANSL_FAULT_ISS_MASK_32BIT: usize = !0x143;
-    const PERM_FAULT_BASE_32BIT: usize = 0x9600004C;
-    const PERM_FAULT_ISS_MASK_32BIT: usize = !0x103;
-}
-
-impl From<usize> for Esr {
-    fn from(esr: usize) -> Self {
-        if esr == Self::EXT_DABT_32BIT {
-            Self::DataAbortSyncExternalAbort
-        } else if esr & Self::TRANSL_FAULT_ISS_MASK_32BIT == Self::TRANSL_FAULT_BASE_32BIT {
-            Self::DataAbortTranslationFault
-        } else if esr & Self::PERM_FAULT_ISS_MASK_32BIT == Self::PERM_FAULT_BASE_32BIT {
-            Self::DataAbortPermissionFault
-        } else {
-            Self::Unknown(esr)
-        }
-    }
-}
-
-impl fmt::Display for Esr {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        match self {
-            Self::DataAbortSyncExternalAbort => write!(f, "Synchronous external abort"),
-            Self::DataAbortTranslationFault => write!(f, "Translation fault"),
-            Self::DataAbortPermissionFault => write!(f, "Permission fault"),
-            Self::Unknown(v) => write!(f, "Unknown exception esr={v:#08x}"),
-        }
-    }
-}
-
-#[inline]
-fn handle_translation_fault(far: usize) -> Result<(), HandleExceptionError> {
-    let mut guard = MEMORY.try_lock().ok_or(HandleExceptionError::PageTableUnavailable)?;
-    let memory = guard.as_mut().ok_or(HandleExceptionError::PageTableNotInitialized)?;
-    Ok(memory.handle_mmio_fault(far)?)
-}
-
-#[inline]
-fn handle_permission_fault(far: usize) -> Result<(), HandleExceptionError> {
-    let mut guard = MEMORY.try_lock().ok_or(HandleExceptionError::PageTableUnavailable)?;
-    let memory = guard.as_mut().ok_or(HandleExceptionError::PageTableNotInitialized)?;
-    Ok(memory.handle_permission_fault(far)?)
-}
-
-fn handle_exception(esr: Esr, far: usize) -> Result<(), HandleExceptionError> {
+fn handle_exception(exception: &ArmException) -> Result<(), HandleExceptionError> {
     // Handle all translation faults on both read and write, and MMIO guard map
     // flagged invalid pages or blocks that caused the exception.
     // Handle permission faults for DBM flagged entries, and flag them as dirty on write.
-    match esr {
-        Esr::DataAbortTranslationFault => handle_translation_fault(far),
-        Esr::DataAbortPermissionFault => handle_permission_fault(far),
+    match exception.esr {
+        Esr::DataAbortTranslationFault => handle_translation_fault(exception.far),
+        Esr::DataAbortPermissionFault => handle_permission_fault(exception.far),
         _ => Err(HandleExceptionError::UnknownException),
     }
 }
 
-#[inline]
-fn handling_uart_exception(esr: Esr, far: usize) -> bool {
-    esr == Esr::DataAbortSyncExternalAbort && page_4kb_of(far) == UART_PAGE
-}
-
 #[no_mangle]
 extern "C" fn sync_exception_current(elr: u64, _spsr: u64) {
     // Disable logging in exception handler to prevent unsafe writes to UART.
     let _guard = logger::suppress();
-    let esr: Esr = read_sysreg!("esr_el1").into();
-    let far = read_sysreg!("far_el1");
 
-    if let Err(e) = handle_exception(esr, far) {
-        // Don't print to the UART if we are handling an exception it could raise.
-        if !handling_uart_exception(esr, far) {
-            eprintln!("sync_exception_current");
-            eprintln!("{e}");
-            eprintln!("{esr}, far={far:#08x}, elr={elr:#08x}");
-        }
+    let exception = ArmException::from_el1_regs();
+    if let Err(e) = handle_exception(&exception) {
+        exception.print("sync_exception_current", e, elr);
         reboot()
     }
 }
diff --git a/pvmfw/src/fdt.rs b/pvmfw/src/fdt.rs
index efb354c..244b192 100644
--- a/pvmfw/src/fdt.rs
+++ b/pvmfw/src/fdt.rs
@@ -124,8 +124,24 @@
     node.setprop(cstr!("bootargs"), bootargs.to_bytes_with_nul())
 }
 
-/// Check if memory range is ok
-fn validate_memory_range(range: &Range<usize>) -> Result<(), RebootReason> {
+/// Reads and validates the memory range in the DT.
+///
+/// Only one memory range is expected with the crosvm setup for now.
+fn read_and_validate_memory_range(fdt: &Fdt) -> Result<Range<usize>, RebootReason> {
+    let mut memory = fdt.memory().map_err(|e| {
+        error!("Failed to read memory range from DT: {e}");
+        RebootReason::InvalidFdt
+    })?;
+    let range = memory.next().ok_or_else(|| {
+        error!("The /memory node in the DT contains no range.");
+        RebootReason::InvalidFdt
+    })?;
+    if memory.next().is_some() {
+        warn!(
+            "The /memory node in the DT contains more than one memory range, \
+             while only one is expected."
+        );
+    }
     let base = range.start;
     if base != MEM_START {
         error!("Memory base address {:#x} is not {:#x}", base, MEM_START);
@@ -142,7 +158,7 @@
         error!("Memory size is 0");
         return Err(RebootReason::InvalidFdt);
     }
-    Ok(())
+    Ok(range)
 }
 
 fn patch_memory_range(fdt: &mut Fdt, memory_range: &Range<usize>) -> libfdt::Result<()> {
@@ -193,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>;
@@ -232,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 })
 }
@@ -543,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],
@@ -600,11 +624,7 @@
         RebootReason::InvalidFdt
     })?;
 
-    let memory_range = fdt.first_memory_range().map_err(|e| {
-        error!("Failed to read memory range from DT: {e}");
-        RebootReason::InvalidFdt
-    })?;
-    validate_memory_range(&memory_range)?;
+    let memory_range = read_and_validate_memory_range(fdt)?;
 
     let bootargs = read_bootargs_from(fdt).map_err(|e| {
         error!("Failed to read bootargs from DT: {e}");
@@ -789,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())?;
@@ -801,7 +821,7 @@
     }
 }
 
-fn read_common_debug_policy(fdt: &Fdt, debug_feature_name: &CStr) -> libfdt::Result<bool> {
+fn has_common_debug_policy(fdt: &Fdt, debug_feature_name: &CStr) -> libfdt::Result<bool> {
     if let Some(node) = fdt.node(cstr!("/avf/guest/common"))? {
         if let Some(value) = node.getprop_u32(debug_feature_name)? {
             return Ok(value == 1);
@@ -811,8 +831,8 @@
 }
 
 fn filter_out_dangerous_bootargs(fdt: &mut Fdt, bootargs: &CStr) -> libfdt::Result<()> {
-    let has_crashkernel = read_common_debug_policy(fdt, cstr!("ramdump"))?;
-    let has_console = read_common_debug_policy(fdt, cstr!("log"))?;
+    let has_crashkernel = has_common_debug_policy(fdt, cstr!("ramdump"))?;
+    let has_console = has_common_debug_policy(fdt, cstr!("log"))?;
 
     let accepted: &[(&str, Box<dyn Fn(Option<&str>) -> bool>)] = &[
         ("panic", Box::new(|v| if let Some(v) = v { v == "=-1" } else { false })),
diff --git a/pvmfw/src/gpt.rs b/pvmfw/src/gpt.rs
index b553705..1060460 100644
--- a/pvmfw/src/gpt.rs
+++ b/pvmfw/src/gpt.rs
@@ -24,9 +24,11 @@
 use uuid::Uuid;
 use virtio_drivers::device::blk::SECTOR_SIZE;
 use vmbase::util::ceiling_div;
-use vmbase::virtio::pci::VirtIOBlk;
+use vmbase::virtio::{pci, HalImpl};
 use zerocopy::FromBytes;
 
+type VirtIOBlk = pci::VirtIOBlk<HalImpl>;
+
 pub enum Error {
     /// VirtIO error during read operation.
     FailedRead(virtio_drivers::Error),
@@ -128,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/pvmfw/src/instance.rs b/pvmfw/src/instance.rs
index 1035559..f2b34da 100644
--- a/pvmfw/src/instance.rs
+++ b/pvmfw/src/instance.rs
@@ -32,6 +32,7 @@
 use vmbase::rand;
 use vmbase::util::ceiling_div;
 use vmbase::virtio::pci::{PciTransportIterator, VirtIOBlk};
+use vmbase::virtio::HalImpl;
 use zerocopy::AsBytes;
 use zerocopy::FromBytes;
 
@@ -183,10 +184,11 @@
 }
 
 fn find_instance_img(pci_root: &mut PciRoot) -> Result<Partition> {
-    for transport in
-        PciTransportIterator::new(pci_root).filter(|t| DeviceType::Block == t.device_type())
+    for transport in PciTransportIterator::<HalImpl>::new(pci_root)
+        .filter(|t| DeviceType::Block == t.device_type())
     {
-        let device = VirtIOBlk::new(transport).map_err(Error::VirtIOBlkCreationFailed)?;
+        let device =
+            VirtIOBlk::<HalImpl>::new(transport).map_err(Error::VirtIOBlkCreationFailed)?;
         match Partition::get_by_name(device, "vm-instance") {
             Ok(Some(p)) => return Ok(p),
             Ok(None) => {}
diff --git a/pvmfw/src/main.rs b/pvmfw/src/main.rs
index 61e2312..ba453e7 100644
--- a/pvmfw/src/main.rs
+++ b/pvmfw/src/main.rs
@@ -95,8 +95,8 @@
     // Set up PCI bus for VirtIO devices.
     let pci_info = PciInfo::from_fdt(fdt).map_err(handle_pci_error)?;
     debug!("PCI: {:#x?}", pci_info);
-    let mut pci_root = pci::initialise(pci_info, MEMORY.lock().as_mut().unwrap()).map_err(|e| {
-        error!("Failed to initialise PCI: {e}");
+    let mut pci_root = pci::initialize(pci_info, MEMORY.lock().as_mut().unwrap()).map_err(|e| {
+        error!("Failed to initialize PCI: {e}");
         RebootReason::InternalError
     })?;
 
diff --git a/pvmfw/src/memory.rs b/pvmfw/src/memory.rs
index 11fcd7c..27ab719 100644
--- a/pvmfw/src/memory.rs
+++ b/pvmfw/src/memory.rs
@@ -17,25 +17,27 @@
 #![deny(unsafe_op_in_unsafe_fn)]
 
 use crate::helpers::PVMFW_PAGE_SIZE;
+use aarch64_paging::paging::VirtualAddress;
 use aarch64_paging::MapError;
+use core::ops::Range;
 use core::result;
 use log::error;
 use vmbase::{
     layout,
-    memory::{MemoryRange, PageTable, SIZE_2MB, SIZE_4KB},
+    memory::{PageTable, SIZE_2MB, SIZE_4KB},
     util::align_up,
 };
 
 /// Returns memory range reserved for the appended payload.
-pub fn appended_payload_range() -> MemoryRange {
-    let start = align_up(layout::binary_end(), SIZE_4KB).unwrap();
+pub fn appended_payload_range() -> Range<VirtualAddress> {
+    let start = align_up(layout::binary_end().0, SIZE_4KB).unwrap();
     // pvmfw is contained in a 2MiB region so the payload can't be larger than the 2MiB alignment.
     let end = align_up(start, SIZE_2MB).unwrap();
-    start..end
+    VirtualAddress(start)..VirtualAddress(end)
 }
 
 /// Region allocated for the stack.
-pub fn stack_range() -> MemoryRange {
+pub fn stack_range() -> Range<VirtualAddress> {
     const STACK_PAGES: usize = 8;
 
     layout::stack_range(STACK_PAGES * PVMFW_PAGE_SIZE)
@@ -46,12 +48,12 @@
 
     // Stack and scratch ranges are explicitly zeroed and flushed before jumping to payload,
     // so dirty state management can be omitted.
-    page_table.map_data(&layout::scratch_range())?;
-    page_table.map_data(&stack_range())?;
-    page_table.map_code(&layout::text_range())?;
-    page_table.map_rodata(&layout::rodata_range())?;
-    page_table.map_data_dbm(&appended_payload_range())?;
-    if let Err(e) = page_table.map_device(&layout::console_uart_range()) {
+    page_table.map_data(&layout::scratch_range().into())?;
+    page_table.map_data(&stack_range().into())?;
+    page_table.map_code(&layout::text_range().into())?;
+    page_table.map_rodata(&layout::rodata_range().into())?;
+    page_table.map_data_dbm(&appended_payload_range().into())?;
+    if let Err(e) = page_table.map_device(&layout::console_uart_range().into()) {
         error!("Failed to remap the UART as a dynamic page table entry: {e}");
         return Err(e);
     }
diff --git a/rialto/Android.bp b/rialto/Android.bp
index 9aa4667..1840278 100644
--- a/rialto/Android.bp
+++ b/rialto/Android.bp
@@ -13,6 +13,7 @@
         "libfdtpci",
         "liblibfdt",
         "liblog_rust_nostd",
+        "libvirtio_drivers",
         "libvmbase",
     ],
 }
diff --git a/rialto/src/error.rs b/rialto/src/error.rs
index 8e2991c..0c1e25d 100644
--- a/rialto/src/error.rs
+++ b/rialto/src/error.rs
@@ -19,7 +19,7 @@
 use fdtpci::PciError;
 use hyp::Error as HypervisorError;
 use libfdt::FdtError;
-use vmbase::memory::MemoryTrackerError;
+use vmbase::{memory::MemoryTrackerError, virtio::pci};
 
 pub type Result<T> = result::Result<T, Error>;
 
@@ -29,14 +29,18 @@
     Hypervisor(HypervisorError),
     /// Failed when attempting to map some range in the page table.
     PageTableMapping(MapError),
-    /// Failed to initialize the logger.
-    LoggerInit,
     /// Invalid FDT.
     InvalidFdt(FdtError),
     /// Invalid PCI.
     InvalidPci(PciError),
     /// Failed memory operation.
     MemoryOperationFailed(MemoryTrackerError),
+    /// Failed to initialize PCI.
+    PciInitializationFailed(pci::PciError),
+    /// Failed to create VirtIO Socket device.
+    VirtIOSocketCreationFailed(virtio_drivers::Error),
+    /// Missing socket device.
+    MissingVirtIOSocketDevice,
 }
 
 impl fmt::Display for Error {
@@ -46,10 +50,14 @@
             Self::PageTableMapping(e) => {
                 write!(f, "Failed when attempting to map some range in the page table: {e}.")
             }
-            Self::LoggerInit => write!(f, "Failed to initialize the logger."),
             Self::InvalidFdt(e) => write!(f, "Invalid FDT: {e}"),
             Self::InvalidPci(e) => write!(f, "Invalid PCI: {e}"),
             Self::MemoryOperationFailed(e) => write!(f, "Failed memory operation: {e}"),
+            Self::PciInitializationFailed(e) => write!(f, "Failed to initialize PCI: {e}"),
+            Self::VirtIOSocketCreationFailed(e) => {
+                write!(f, "Failed to create VirtIO Socket device: {e}")
+            }
+            Self::MissingVirtIOSocketDevice => write!(f, "Missing VirtIO Socket device."),
         }
     }
 }
diff --git a/rialto/src/exceptions.rs b/rialto/src/exceptions.rs
index 61f7846..b806b08 100644
--- a/rialto/src/exceptions.rs
+++ b/rialto/src/exceptions.rs
@@ -14,14 +14,37 @@
 
 //! Exception handlers.
 
-use core::arch::asm;
-use vmbase::{console::emergency_write_str, eprintln, power::reboot};
+use vmbase::{
+    console::emergency_write_str,
+    eprintln,
+    exceptions::{ArmException, Esr, HandleExceptionError},
+    logger,
+    memory::{handle_permission_fault, handle_translation_fault},
+    power::reboot,
+    read_sysreg,
+};
+
+fn handle_exception(exception: &ArmException) -> Result<(), HandleExceptionError> {
+    // Handle all translation faults on both read and write, and MMIO guard map
+    // flagged invalid pages or blocks that caused the exception.
+    // Handle permission faults for DBM flagged entries, and flag them as dirty on write.
+    match exception.esr {
+        Esr::DataAbortTranslationFault => handle_translation_fault(exception.far),
+        Esr::DataAbortPermissionFault => handle_permission_fault(exception.far),
+        _ => Err(HandleExceptionError::UnknownException),
+    }
+}
 
 #[no_mangle]
-extern "C" fn sync_exception_current() {
-    emergency_write_str("sync_exception_current\n");
-    print_esr();
-    reboot();
+extern "C" fn sync_exception_current(elr: u64, _spsr: u64) {
+    // Disable logging in exception handler to prevent unsafe writes to UART.
+    let _guard = logger::suppress();
+
+    let exception = ArmException::from_el1_regs();
+    if let Err(e) = handle_exception(&exception) {
+        exception.print("sync_exception_current", e, elr);
+        reboot()
+    }
 }
 
 #[no_mangle]
@@ -71,9 +94,6 @@
 
 #[inline]
 fn print_esr() {
-    let mut esr: u64;
-    unsafe {
-        asm!("mrs {esr}, esr_el1", esr = out(reg) esr);
-    }
+    let esr = read_sysreg!("esr_el1");
     eprintln!("esr={:#08x}", esr);
 }
diff --git a/rialto/src/main.rs b/rialto/src/main.rs
index ce83624..bbc9997 100644
--- a/rialto/src/main.rs
+++ b/rialto/src/main.rs
@@ -24,49 +24,40 @@
 
 use crate::error::{Error, Result};
 use core::num::NonZeroUsize;
-use core::result;
 use core::slice;
 use fdtpci::PciInfo;
-use hyp::{get_hypervisor, HypervisorCap, KvmError};
+use hyp::{get_mem_sharer, get_mmio_guard};
 use libfdt::FdtError;
 use log::{debug, error, info};
+use virtio_drivers::{
+    transport::{pci::bus::PciRoot, DeviceType, Transport},
+    Hal,
+};
 use vmbase::{
     configure_heap,
     fdt::SwiotlbInfo,
     layout::{self, crosvm},
     main,
-    memory::{MemoryTracker, PageTable, MEMORY, PAGE_SIZE, SIZE_64KB},
+    memory::{MemoryTracker, PageTable, MEMORY, PAGE_SIZE, SIZE_128KB},
     power::reboot,
+    virtio::{
+        pci::{self, PciTransportIterator, VirtIOSocket},
+        HalImpl,
+    },
 };
 
 fn new_page_table() -> Result<PageTable> {
     let mut page_table = PageTable::default();
 
-    page_table.map_device(&crosvm::MMIO_RANGE)?;
-    page_table.map_data(&layout::scratch_range())?;
-    page_table.map_data(&layout::stack_range(40 * PAGE_SIZE))?;
-    page_table.map_code(&layout::text_range())?;
-    page_table.map_rodata(&layout::rodata_range())?;
-    page_table.map_device(&layout::console_uart_range())?;
+    page_table.map_data(&layout::scratch_range().into())?;
+    page_table.map_data(&layout::stack_range(40 * PAGE_SIZE).into())?;
+    page_table.map_code(&layout::text_range().into())?;
+    page_table.map_rodata(&layout::rodata_range().into())?;
+    page_table.map_device(&layout::console_uart_range().into())?;
 
     Ok(page_table)
 }
 
-fn try_init_logger() -> Result<bool> {
-    let mmio_guard_supported = match get_hypervisor().mmio_guard_init() {
-        // pKVM blocks MMIO by default, we need to enable MMIO guard to support logging.
-        Ok(()) => {
-            get_hypervisor().mmio_guard_map(vmbase::console::BASE_ADDRESS)?;
-            true
-        }
-        // MMIO guard enroll is not supported in unprotected VM.
-        Err(hyp::Error::MmioGuardNotsupported) => false,
-        Err(e) => return Err(e.into()),
-    };
-    vmbase::logger::init(log::LevelFilter::Debug).map_err(|_| Error::LoggerInit)?;
-    Ok(mmio_guard_supported)
-}
-
 /// # Safety
 ///
 /// Behavior is undefined if any of the following conditions are violated:
@@ -91,8 +82,6 @@
     let fdt = unsafe { slice::from_raw_parts(fdt_range.start as *mut u8, fdt_range.len()) };
     // We do not need to validate the DT since it is already validated in pvmfw.
     let fdt = libfdt::Fdt::from_slice(fdt)?;
-    let pci_info = PciInfo::from_fdt(fdt)?;
-    debug!("PCI: {pci_info:#x?}");
 
     let memory_range = fdt.first_memory_range()?;
     MEMORY.lock().as_mut().unwrap().shrink(&memory_range).map_err(|e| {
@@ -100,14 +89,14 @@
         e
     })?;
 
-    if get_hypervisor().has_cap(HypervisorCap::DYNAMIC_MEM_SHARE) {
-        let granule = memory_protection_granule()?;
+    if let Some(mem_sharer) = get_mem_sharer() {
+        let granule = mem_sharer.granule()?;
         MEMORY.lock().as_mut().unwrap().init_dynamic_shared_pool(granule).map_err(|e| {
             error!("Failed to initialize dynamically shared pool.");
             e
         })?;
-    } else {
-        let range = SwiotlbInfo::new_from_fdt(fdt)?.fixed_range().ok_or_else(|| {
+    } else if let Ok(swiotlb_info) = SwiotlbInfo::new_from_fdt(fdt) {
+        let range = swiotlb_info.fixed_range().ok_or_else(|| {
             error!("Pre-shared pool range not specified in swiotlb node");
             Error::from(FdtError::BadValue)
         })?;
@@ -115,54 +104,65 @@
             error!("Failed to initialize pre-shared pool.");
             e
         })?;
+    } else {
+        info!("No MEM_SHARE capability detected or swiotlb found: allocating buffers from heap.");
+        MEMORY.lock().as_mut().unwrap().init_heap_shared_pool().map_err(|e| {
+            error!("Failed to initialize heap-based pseudo-shared pool.");
+            e
+        })?;
     }
+
+    let pci_info = PciInfo::from_fdt(fdt)?;
+    debug!("PCI: {pci_info:#x?}");
+    let mut pci_root = pci::initialize(pci_info, MEMORY.lock().as_mut().unwrap())
+        .map_err(Error::PciInitializationFailed)?;
+    debug!("PCI root: {pci_root:#x?}");
+    let socket_device = find_socket_device::<HalImpl>(&mut pci_root)?;
+    debug!("Found socket device: guest cid = {:?}", socket_device.guest_cid());
     Ok(())
 }
 
-fn memory_protection_granule() -> result::Result<usize, hyp::Error> {
-    match get_hypervisor().memory_protection_granule() {
-        Ok(granule) => Ok(granule),
-        // Take the default page size when KVM call is not supported in non-protected VMs.
-        Err(hyp::Error::KvmError(KvmError::NotSupported, _)) => Ok(PAGE_SIZE),
-        Err(e) => Err(e),
-    }
+fn find_socket_device<T: Hal>(pci_root: &mut PciRoot) -> Result<VirtIOSocket<T>> {
+    PciTransportIterator::<T>::new(pci_root)
+        .find(|t| DeviceType::Socket == t.device_type())
+        .map(VirtIOSocket::<T>::new)
+        .transpose()
+        .map_err(Error::VirtIOSocketCreationFailed)?
+        .ok_or(Error::MissingVirtIOSocketDevice)
 }
 
-fn try_unshare_all_memory(mmio_guard_supported: bool) -> Result<()> {
+fn try_unshare_all_memory() -> Result<()> {
     info!("Starting unsharing memory...");
 
     // No logging after unmapping UART.
-    if mmio_guard_supported {
-        get_hypervisor().mmio_guard_unmap(vmbase::console::BASE_ADDRESS)?;
+    if let Some(mmio_guard) = get_mmio_guard() {
+        mmio_guard.unmap(vmbase::console::BASE_ADDRESS)?;
     }
     // Unshares all memory and deactivates page table.
     drop(MEMORY.lock().take());
     Ok(())
 }
 
-fn unshare_all_memory(mmio_guard_supported: bool) {
-    if let Err(e) = try_unshare_all_memory(mmio_guard_supported) {
+fn unshare_all_memory() {
+    if let Err(e) = try_unshare_all_memory() {
         error!("Failed to unshare the memory: {e}");
     }
 }
 
 /// Entry point for Rialto.
 pub fn main(fdt_addr: u64, _a1: u64, _a2: u64, _a3: u64) {
-    let Ok(mmio_guard_supported) = try_init_logger() else {
-        // Don't log anything if the logger initialization fails.
-        reboot();
-    };
+    log::set_max_level(log::LevelFilter::Debug);
     // SAFETY: `fdt_addr` is supposed to be a valid pointer and points to
     // a valid `Fdt`.
     match unsafe { try_main(fdt_addr as usize) } {
-        Ok(()) => unshare_all_memory(mmio_guard_supported),
+        Ok(()) => unshare_all_memory(),
         Err(e) => {
             error!("Rialto failed with {e}");
-            unshare_all_memory(mmio_guard_supported);
+            unshare_all_memory();
             reboot()
         }
     }
 }
 
 main!(main);
-configure_heap!(SIZE_64KB);
+configure_heap!(SIZE_128KB);
diff --git a/rialto/tests/test.rs b/rialto/tests/test.rs
index 7048b44..98c935d 100644
--- a/rialto/tests/test.rs
+++ b/rialto/tests/test.rs
@@ -114,8 +114,15 @@
         taskProfiles: vec![],
         gdbPort: 0, // No gdb
     });
-    let vm = VmInstance::create(service.as_ref(), &config, Some(console), Some(log), None)
-        .context("Failed to create VM")?;
+    let vm = VmInstance::create(
+        service.as_ref(),
+        &config,
+        Some(console),
+        /* consoleIn */ None,
+        Some(log),
+        None,
+    )
+    .context("Failed to create VM")?;
 
     vm.start().context("Failed to start VM")?;
 
diff --git a/tests/aidl/com/android/microdroid/testservice/ITestService.aidl b/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
index a6f1c80..8d467cd 100644
--- a/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
+++ b/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
@@ -70,6 +70,9 @@
     /** Requests the VM to asynchronously call appCallback.setVmCallback() */
     void requestCallback(IAppCallback appCallback);
 
+    /** Read a line from /dev/console */
+    String readLineFromConsole();
+
     /**
      * Request the service to exit, triggering the termination of the VM. This may cause any
      * requests in flight to fail.
diff --git a/tests/benchmark/Android.bp b/tests/benchmark/Android.bp
index 9c512bf..90ba575 100644
--- a/tests/benchmark/Android.bp
+++ b/tests/benchmark/Android.bp
@@ -26,6 +26,7 @@
     sdk_version: "test_current",
     use_embedded_native_libs: true,
     compile_multilib: "64",
+    required: ["perf-setup"],
     host_required: ["MicrodroidTestPreparer"],
 }
 
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 aed28a8..625f26a 100644
--- a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
+++ b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
@@ -78,6 +78,7 @@
     private static final String TAG = "MicrodroidBenchmarks";
     private static final String METRIC_NAME_PREFIX = getMetricPrefix() + "microdroid/";
     private static final int IO_TEST_TRIAL_COUNT = 5;
+    private static final int TEST_TRIAL_COUNT = 5;
     private static final long ONE_MEBI = 1024 * 1024;
 
     @Rule public Timeout globalTimeout = Timeout.seconds(300);
@@ -767,4 +768,35 @@
         }
         reportMetrics(requestLatencies, "latency/vsock", "us");
     }
+
+    @Test
+    public void testVmKillTime() throws Exception {
+        VirtualMachineConfig config =
+                newVmConfigBuilder()
+                        .setPayloadConfigPath("assets/vm_config_io.json")
+                        .setDebugLevel(DEBUG_LEVEL_NONE)
+                        .build();
+        List<Double> vmKillTime = new ArrayList<>(TEST_TRIAL_COUNT);
+
+        for (int i = 0; i < TEST_TRIAL_COUNT; ++i) {
+            VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_kill_time" + i, config);
+            VmEventListener listener =
+                    new VmEventListener() {
+                        @Override
+                        public void onPayloadReady(VirtualMachine vm) {
+                            long start = System.nanoTime();
+                            try {
+                                vm.stop();
+                            } catch (Exception e) {
+                                Log.e(TAG, "Error in vm.stop():" + e);
+                                throw new RuntimeException(e);
+                            }
+                            vmKillTime.add((double) (System.nanoTime() - start) / NANO_TO_MICRO);
+                            super.onPayloadReady(vm);
+                        }
+                    };
+            listener.runToFinish(TAG, vm);
+        }
+        reportMetrics(vmKillTime, "vm_kill_time", "microsecond");
+    }
 }
diff --git a/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java b/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java
index 8c6218c..f98d1d9 100644
--- a/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java
+++ b/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java
@@ -60,9 +60,6 @@
     // Files that define the "test" instance of CompOS
     private static final String COMPOS_TEST_ROOT = "/data/misc/apexdata/com.android.compos/test/";
 
-    private static final String SYSTEM_SERVER_COMPILER_FILTER_PROP_NAME =
-            "dalvik.vm.systemservercompilerfilter";
-
     private static final String BOOTLOADER_TIME_PROP_NAME = "ro.boot.boottime";
     private static final String BOOTLOADER_PREFIX = "bootloader-";
     private static final String BOOTLOADER_TIME = "bootloader_time";
@@ -74,7 +71,6 @@
     private static final int COMPILE_STAGED_APEX_RETRY_INTERVAL_MS = 10 * 1000;
     private static final int COMPILE_STAGED_APEX_TIMEOUT_SEC = 540;
     private static final int BOOT_COMPLETE_TIMEOUT_MS = 10 * 60 * 1000;
-    private static final double NANOS_IN_SEC = 1_000_000_000.0;
     private static final int ROUND_COUNT = 5;
     private static final int ROUND_IGNORE_STARTUP_TIME = 3;
     private static final String APK_NAME = "MicrodroidTestApp.apk";
@@ -262,32 +258,34 @@
                         .memoryMib(vm_mem_mb)
                         .cpuTopology("match_host")
                         .build(device);
-        microdroidDevice.waitForBootComplete(30000);
-        microdroidDevice.enableAdbRoot();
-
-        CommandRunner microdroid = new CommandRunner(microdroidDevice);
-
-        microdroid.run("mkdir -p /mnt/ramdisk && chmod 777 /mnt/ramdisk");
-        microdroid.run("mount -t tmpfs -o size=32G tmpfs /mnt/ramdisk");
-
-        // Allocate memory for the VM until it fails and make sure that we touch
-        // the allocated memory in the guest to be able to create stage2 fragmentation.
         try {
-            microdroid.tryRun(
-                    String.format(
-                            "cd /mnt/ramdisk && truncate -s %dM sprayMemory"
-                                    + " && dd if=/dev/zero of=sprayMemory bs=1MB count=%d",
-                            vm_mem_mb, vm_mem_mb));
-        } catch (Exception ex) {
-        }
+            microdroidDevice.waitForBootComplete(30000);
+            microdroidDevice.enableAdbRoot();
 
-        // Run the app during the VM run and collect cold startup time.
-        for (int i = 0; i < ROUND_COUNT; i++) {
-            AmStartupTimeCmdParser duringVmStartApp = getColdRunStartupTimes(android, pkgName);
-            metricColector.addStartupTimeMetricDuringVmRun(duringVmStartApp);
-        }
+            CommandRunner microdroid = new CommandRunner(microdroidDevice);
 
-        device.shutdownMicrodroid(microdroidDevice);
+            microdroid.run("mkdir -p /mnt/ramdisk && chmod 777 /mnt/ramdisk");
+            microdroid.run("mount -t tmpfs -o size=32G tmpfs /mnt/ramdisk");
+
+            // Allocate memory for the VM until it fails and make sure that we touch
+            // the allocated memory in the guest to be able to create stage2 fragmentation.
+            try {
+                microdroid.tryRun(
+                        String.format(
+                                "cd /mnt/ramdisk && truncate -s %dM sprayMemory"
+                                        + " && dd if=/dev/zero of=sprayMemory bs=1MB count=%d",
+                                vm_mem_mb, vm_mem_mb));
+            } catch (Exception expected) {
+            }
+
+            // Run the app during the VM run and collect cold startup time.
+            for (int i = 0; i < ROUND_COUNT; i++) {
+                AmStartupTimeCmdParser duringVmStartApp = getColdRunStartupTimes(android, pkgName);
+                metricColector.addStartupTimeMetricDuringVmRun(duringVmStartApp);
+            }
+        } finally {
+            device.shutdownMicrodroid(microdroidDevice);
+        }
 
         // Run the app after the VM run and collect cold startup time.
         for (int i = 0; i < ROUND_COUNT; i++) {
@@ -304,12 +302,12 @@
             String[] lines = startAppLog.split("[\r\n]+");
             mTotalTime = mWaitTime = 0;
 
-            for (int i = 0; i < lines.length; i++) {
-                if (lines[i].contains("TotalTime:")) {
-                    mTotalTime = Integer.parseInt(lines[i].replaceAll("\\D+", ""));
+            for (String line : lines) {
+                if (line.contains("TotalTime:")) {
+                    mTotalTime = Integer.parseInt(line.replaceAll("\\D+", ""));
                 }
-                if (lines[i].contains("WaitTime:")) {
-                    mWaitTime = Integer.parseInt(lines[i].replaceAll("\\D+", ""));
+                if (line.contains("WaitTime:")) {
+                    mWaitTime = Integer.parseInt(line.replaceAll("\\D+", ""));
                 }
             }
         }
@@ -365,9 +363,9 @@
         String content = android.runForResult("cat /proc/meminfo").getStdout().trim();
         String[] lines = content.split("[\r\n]+");
 
-        for (int i = 0; i < lines.length; i++) {
-            if (lines[i].contains("MemFree:")) {
-                freeMemory = Integer.parseInt(lines[i].replaceAll("\\D+", "")) / 1024;
+        for (String line : lines) {
+            if (line.contains("MemFree:")) {
+                freeMemory = Integer.parseInt(line.replaceAll("\\D+", "")) / 1024;
                 return freeMemory;
             }
         }
@@ -416,7 +414,7 @@
 
         CommandRunner android = new CommandRunner(getDevice());
         String result = android.run("dmesg");
-        Pattern pattern = Pattern.compile("\\[(.*)\\].*sys.boot_completed=1.*");
+        Pattern pattern = Pattern.compile("\\[(.*)].*sys.boot_completed=1.*");
         for (String line : result.split("[\r\n]+")) {
             Matcher matcher = pattern.matcher(line);
             if (matcher.find()) {
@@ -568,7 +566,7 @@
     private void compileStagedApex(int timeoutSec) throws Exception {
 
         long timeStart = System.currentTimeMillis();
-        long timeEnd = timeStart + timeoutSec * 1000;
+        long timeEnd = timeStart + timeoutSec * 1000L;
 
         while (true) {
 
@@ -599,7 +597,7 @@
     private void reInstallApex(int timeoutSec) throws Exception {
 
         long timeStart = System.currentTimeMillis();
-        long timeEnd = timeStart + timeoutSec * 1000;
+        long timeEnd = timeStart + timeoutSec * 1000L;
 
         while (true) {
 
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 7e6080f..e6d90ea 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
@@ -456,6 +456,7 @@
         public long[] mTimings;
         public int mFileMode;
         public int mMountFlags;
+        public String mConsoleInput;
 
         public void assertNoException() {
             if (mException != null) {
diff --git a/tests/hostside/java/com/android/microdroid/test/DebugPolicyHostTests.java b/tests/hostside/java/com/android/microdroid/test/DebugPolicyHostTests.java
index 014f9f0..9cf28c7 100644
--- a/tests/hostside/java/com/android/microdroid/test/DebugPolicyHostTests.java
+++ b/tests/hostside/java/com/android/microdroid/test/DebugPolicyHostTests.java
@@ -115,7 +115,7 @@
                 mAndroidDevice.supportsMicrodroid(/* protectedVm= */ true));
         assumeFalse("Test requires setprop for using custom pvmfw and adb root", isUserBuild());
 
-        mAndroidDevice.enableAdbRoot();
+        assumeTrue("Skip if adb root fails", mAndroidDevice.enableAdbRoot());
 
         // tradefed copies the test artfacts under /tmp when running tests,
         // so we should *find* the artifacts with the file name.
diff --git a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
index 1f5fcee..092325e 100644
--- a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
+++ b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
@@ -42,7 +42,6 @@
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.TestDevice;
-import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestMetrics;
 import com.android.tradefed.util.CommandResult;
@@ -55,7 +54,6 @@
 import org.json.JSONObject;
 import org.junit.After;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TestName;
@@ -63,12 +61,8 @@
 import org.xml.sax.Attributes;
 import org.xml.sax.helpers.DefaultHandler;
 
-import java.io.BufferedReader;
 import java.io.ByteArrayInputStream;
 import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
 import java.io.PipedInputStream;
 import java.io.PipedOutputStream;
 import java.util.ArrayList;
@@ -76,7 +70,6 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
-import java.util.Optional;
 import java.util.concurrent.Callable;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Function;
@@ -96,15 +89,11 @@
 
     private static final int BOOT_COMPLETE_TIMEOUT = 30000; // 30 seconds
 
-    private static final Pattern sCIDPattern = Pattern.compile("with CID (\\d+)");
-
     private static class VmInfo {
         final Process mProcess;
-        final String mCid;
 
-        VmInfo(Process process, String cid) {
+        VmInfo(Process process) {
             mProcess = process;
-            mCid = cid;
         }
     }
 
@@ -377,44 +366,14 @@
 
         PipedInputStream pis = new PipedInputStream();
         Process process = RunUtil.getDefault().runCmdInBackground(args, new PipedOutputStream(pis));
-        return new VmInfo(process, extractCidFrom(pis));
-    }
-
-    private static Optional<String> tryExtractCidFrom(String str) {
-        Matcher matcher = sCIDPattern.matcher(str);
-        if (matcher.find()) {
-            return Optional.of(matcher.group(1));
-        }
-        return Optional.empty();
-    }
-
-    private static String extractCidFrom(InputStream input) throws IOException {
-        String cid = null;
-        String line;
-        try (BufferedReader out = new BufferedReader(new InputStreamReader(input))) {
-            while ((line = out.readLine()) != null) {
-                CLog.i("VM output: " + line);
-                Optional<String> result = tryExtractCidFrom(line);
-                if (result.isPresent()) {
-                    cid = result.get();
-                    break;
-                }
-            }
-        }
-        assertWithMessage("The output does not contain the expected pattern for CID.")
-                .that(cid)
-                .isNotNull();
-        return cid;
+        return new VmInfo(process);
     }
 
     @Test
     @CddTest(requirements = {"9.17/C-2-1", "9.17/C-2-2", "9.17/C-2-6"})
     public void protectedVmRunsPvmfw() throws Exception {
         // Arrange
-        boolean protectedVm = true;
-        assumeTrue(
-                "Skip if protected VMs are not supported",
-                getAndroidDevice().supportsMicrodroid(protectedVm));
+        assumeProtectedVmSupported();
         final String configPath = "assets/vm_config_apex.json";
 
         // Act
@@ -423,7 +382,7 @@
                         .debugLevel("full")
                         .memoryMib(minMemorySize())
                         .cpuTopology("match_host")
-                        .protectedVm(protectedVm)
+                        .protectedVm(true)
                         .build(getAndroidDevice());
 
         // Assert
@@ -442,16 +401,16 @@
     @CddTest(requirements = {"9.17/C-2-1", "9.17/C-2-2", "9.17/C-2-6"})
     public void protectedVmWithImageSignedWithDifferentKeyRunsPvmfw() throws Exception {
         // Arrange
-        boolean protectedVm = true;
-        assumeTrue(
-                "Skip if protected VMs are not supported",
-                getAndroidDevice().supportsMicrodroid(protectedVm));
+        assumeProtectedVmSupported();
         File key = findTestFile("test.com.android.virt.pem");
 
         // Act
         VmInfo vmInfo =
                 runMicrodroidWithResignedImages(
-                        key, /*keyOverrides=*/ Map.of(), protectedVm, /*updateBootconfigs=*/ true);
+                        key,
+                        /*keyOverrides=*/ Map.of(),
+                        /*isProtected=*/ true,
+                        /*updateBootconfigs=*/ true);
 
         // Assert
         vmInfo.mProcess.waitFor(5L, TimeUnit.SECONDS);
@@ -466,6 +425,7 @@
     @CddTest(requirements = {"9.17/C-2-2", "9.17/C-2-6"})
     public void testBootSucceedsWhenNonProtectedVmStartsWithImagesSignedWithDifferentKey()
             throws Exception {
+        assumeNonProtectedVmSupported();
         File key = findTestFile("test.com.android.virt.pem");
         Map<String, File> keyOverrides = Map.of();
         VmInfo vmInfo =
@@ -482,6 +442,8 @@
     @Test
     @CddTest(requirements = {"9.17/C-2-2", "9.17/C-2-6"})
     public void testBootFailsWhenVbMetaDigestDoesNotMatchBootconfig() throws Exception {
+        // protectedVmWithImageSignedWithDifferentKeyRunsPvmfw() is the protected case.
+        assumeNonProtectedVmSupported();
         // Sign everything with key1 except vbmeta
         File key = findTestFile("test.com.android.virt.pem");
         // To be able to stop it, it should be a daemon.
@@ -496,7 +458,7 @@
         vmInfo.mProcess.destroy();
     }
 
-    private void waitForCrosvmExit(CommandRunner android) throws Exception {
+    private void waitForCrosvmExit(CommandRunner android, String testStartTime) throws Exception {
         // TODO: improve crosvm exit check. b/258848245
         android.runWithTimeout(
                 15000,
@@ -504,10 +466,12 @@
                 "-m",
                 "1",
                 "-e",
-                "'virtualizationmanager::crosvm.*exited with status exit status:'");
+                "'virtualizationmanager::crosvm.*exited with status exit status:'",
+                "-T",
+                "'" + testStartTime + "'");
     }
 
-    private boolean isTombstoneReceivedFromHostLogcat() throws Exception {
+    private boolean isTombstoneReceivedFromHostLogcat(String testStartTime) throws Exception {
         // Note this method relies on logcat values being printed by the receiver on host
         // userspace crash log: virtualizationservice/src/aidl.rs
         // kernel ramdump log: virtualizationmanager/src/crosvm.rs
@@ -526,12 +490,17 @@
                         "-m",
                         "1",
                         "-e",
-                        ramdumpRegex);
+                        ramdumpRegex,
+                        "-T",
+                        testStartTime);
         return !result.trim().isEmpty();
     }
 
     private boolean isTombstoneGeneratedWithCmd(
             boolean protectedVm, String configPath, String... crashCommand) throws Exception {
+        CommandRunner android = new CommandRunner(getDevice());
+        String testStartTime = android.runWithTimeout(1000, "date", "'+%Y-%m-%d %H:%M:%S.%N'");
+
         mMicrodroidDevice =
                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
                         .debugLevel("full")
@@ -546,17 +515,28 @@
         microdroid.run(crashCommand);
 
         // check until microdroid is shut down
-        CommandRunner android = new CommandRunner(getDevice());
-        waitForCrosvmExit(android);
+        waitForCrosvmExit(android, testStartTime);
 
-        return isTombstoneReceivedFromHostLogcat();
+        return isTombstoneReceivedFromHostLogcat(testStartTime);
     }
 
     @Test
-    public void testTombstonesAreGeneratedUponUserspaceCrash() throws Exception {
+    public void testTombstonesAreGeneratedUponUserspaceCrashOnNonPvm() throws Exception {
+        assumeNonProtectedVmSupported();
+        testTombstonesAreGeneratedUponUserspaceCrash(false);
+    }
+
+    @Test
+    public void testTombstonesAreGeneratedUponUserspaceCrashOnPvm() throws Exception {
+        assumeProtectedVmSupported();
+        testTombstonesAreGeneratedUponUserspaceCrash(true);
+    }
+
+    private void testTombstonesAreGeneratedUponUserspaceCrash(boolean protectedVm)
+            throws Exception {
         assertThat(
                         isTombstoneGeneratedWithCmd(
-                                false,
+                                protectedVm,
                                 "assets/vm_config.json",
                                 "kill",
                                 "-SIGSEGV",
@@ -565,10 +545,24 @@
     }
 
     @Test
-    public void testTombstonesAreNotGeneratedIfNotExportedUponUserspaceCrash() throws Exception {
+    public void testTombstonesAreNotGeneratedIfNotExportedUponUserspaceCrashOnNonPvm()
+            throws Exception {
+        assumeNonProtectedVmSupported();
+        testTombstonesAreNotGeneratedIfNotExportedUponUserspaceCrash(false);
+    }
+
+    @Test
+    public void testTombstonesAreNotGeneratedIfNotExportedUponUserspaceCrashOnPvm()
+            throws Exception {
+        assumeProtectedVmSupported();
+        testTombstonesAreNotGeneratedIfNotExportedUponUserspaceCrash(true);
+    }
+
+    private void testTombstonesAreNotGeneratedIfNotExportedUponUserspaceCrash(boolean protectedVm)
+            throws Exception {
         assertThat(
                         isTombstoneGeneratedWithCmd(
-                                false,
+                                protectedVm,
                                 "assets/vm_config_no_tombstone.json",
                                 "kill",
                                 "-SIGSEGV",
@@ -592,21 +586,21 @@
 
     @Test
     public void testTombstonesAreGeneratedUponKernelCrashOnNonPvm() throws Exception {
-        testTombstonesAreGeneratedUponKernelCrash(false);
+        assumeNonProtectedVmSupported();
+        testTombstonesAreGeneratedUponKernelCrash(/* protectedVm=*/ false);
     }
 
     @Test
     public void testTombstonesAreGeneratedUponKernelCrashOnPvm() throws Exception {
-        assumeTrue(
-                "Protected VMs are not supported",
-                getAndroidDevice().supportsMicrodroid(/*protectedVm=*/ true));
-        testTombstonesAreGeneratedUponKernelCrash(true);
+        assumeProtectedVmSupported();
+        testTombstonesAreGeneratedUponKernelCrash(/* protectedVm=*/ true);
     }
 
-    private boolean isTombstoneGeneratedWithVmRunApp(boolean debuggable, String... additionalArgs)
-            throws Exception {
+    private boolean isTombstoneGeneratedWithVmRunApp(
+            boolean protectedVm, boolean debuggable, String... additionalArgs) throws Exception {
         // we can't use microdroid builder as it wants ADB connection (debuggable)
         CommandRunner android = new CommandRunner(getDevice());
+        String testStartTime = android.runWithTimeout(1000, "date", "'+%Y-%m-%d %H:%M:%S.%N'");
 
         android.run("rm", "-rf", TEST_ROOT + "*");
         android.run("mkdir", "-p", TEST_ROOT + "*");
@@ -624,44 +618,114 @@
                                 apkPath,
                                 idsigPath,
                                 instanceImgPath));
+        if (protectedVm) {
+            cmd.add("--protected");
+        }
         Collections.addAll(cmd, additionalArgs);
 
         android.run(cmd.toArray(new String[0]));
-        return isTombstoneReceivedFromHostLogcat();
+        return isTombstoneReceivedFromHostLogcat(testStartTime);
     }
 
-    private boolean isTombstoneGeneratedWithCrashPayload(boolean debuggable) throws Exception {
+    private boolean isTombstoneGeneratedWithCrashPayload(boolean protectedVm, boolean debuggable)
+            throws Exception {
         return isTombstoneGeneratedWithVmRunApp(
-                debuggable, "--payload-binary-name", "MicrodroidCrashNativeLib.so");
+                protectedVm, debuggable, "--payload-binary-name", "MicrodroidCrashNativeLib.so");
     }
 
     @Test
-    public void testTombstonesAreGeneratedWithCrashPayload() throws Exception {
-        assertThat(isTombstoneGeneratedWithCrashPayload(true /* debuggable */)).isTrue();
+    public void testTombstonesAreGeneratedWithCrashPayloadOnPvm() throws Exception {
+        assumeProtectedVmSupported();
+        assertThat(
+                        isTombstoneGeneratedWithCrashPayload(
+                                /*protectedVm=*/ true, /*debuggable=*/ true))
+                .isTrue();
     }
 
     @Test
-    public void testTombstonesAreNotGeneratedWithCrashPayloadWhenNonDebuggable() throws Exception {
-        assertThat(isTombstoneGeneratedWithCrashPayload(false /* debuggable */)).isFalse();
+    public void testTombstonesAreGeneratedWithCrashPayloadOnNonPvm() throws Exception {
+        assumeNonProtectedVmSupported();
+        assertThat(
+                        isTombstoneGeneratedWithCrashPayload(
+                                /*protectedVm=*/ false, /*debuggable=*/ true))
+                .isTrue();
     }
 
-    private boolean isTombstoneGeneratedWithCrashConfig(boolean debuggable) throws Exception {
+    @Test
+    public void testTombstonesAreNotGeneratedWithCrashPayloadWhenNonDebuggableOnPvm()
+            throws Exception {
+        assumeProtectedVmSupported();
+        assertThat(
+                        isTombstoneGeneratedWithCrashPayload(
+                                /*protectedVm=*/ true, /*debuggable=*/ false))
+                .isFalse();
+    }
+
+    @Test
+    public void testTombstonesAreNotGeneratedWithCrashPayloadWhenNonDebuggableOnNonPvm()
+            throws Exception {
+        assumeNonProtectedVmSupported();
+        assertThat(
+                        isTombstoneGeneratedWithCrashPayload(
+                                /*protectedVm=*/ false, /*debuggable=*/ false))
+                .isFalse();
+    }
+
+    private boolean isTombstoneGeneratedWithCrashConfig(boolean protectedVm, boolean debuggable)
+            throws Exception {
         return isTombstoneGeneratedWithVmRunApp(
-                debuggable, "--config-path", "assets/vm_config_crash.json");
+                protectedVm, debuggable, "--config-path", "assets/vm_config_crash.json");
     }
 
     @Test
-    public void testTombstonesAreGeneratedWithCrashConfig() throws Exception {
-        assertThat(isTombstoneGeneratedWithCrashConfig(true /* debuggable */)).isTrue();
+    public void testTombstonesAreGeneratedWithCrashConfigOnPvm() throws Exception {
+        assumeProtectedVmSupported();
+        assertThat(isTombstoneGeneratedWithCrashConfig(/*protectedVm=*/ true, /*debuggable=*/ true))
+                .isTrue();
     }
 
     @Test
-    public void testTombstonesAreNotGeneratedWithCrashConfigWhenNonDebuggable() throws Exception {
-        assertThat(isTombstoneGeneratedWithCrashConfig(false /* debuggable */)).isFalse();
+    public void testTombstonesAreGeneratedWithCrashConfigOnNonPvm() throws Exception {
+        assumeNonProtectedVmSupported();
+        assertThat(
+                        isTombstoneGeneratedWithCrashConfig(
+                                /*protectedVm=*/ false, /*debuggable=*/ true))
+                .isTrue();
     }
 
     @Test
-    public void testTelemetryPushedAtoms() throws Exception {
+    public void testTombstonesAreNotGeneratedWithCrashConfigWhenNonDebuggableOnPvm()
+            throws Exception {
+        assumeProtectedVmSupported();
+        assertThat(
+                        isTombstoneGeneratedWithCrashConfig(
+                                /*protectedVm=*/ true, /*debuggable=*/ false))
+                .isFalse();
+    }
+
+    @Test
+    public void testTombstonesAreNotGeneratedWithCrashConfigWhenNonDebuggableOnNonPvm()
+            throws Exception {
+        assumeNonProtectedVmSupported();
+        assertThat(
+                        isTombstoneGeneratedWithCrashConfig(
+                                /*protectedVm=*/ false, /*debuggable=*/ false))
+                .isFalse();
+    }
+
+    @Test
+    public void testTelemetryPushedAtomsOnNonPvm() throws Exception {
+        assumeNonProtectedVmSupported();
+        testTelemetryPushedAtoms(false);
+    }
+
+    @Test
+    public void testTelemetryPushedAtomsOnPvm() throws Exception {
+        assumeProtectedVmSupported();
+        testTelemetryPushedAtoms(true);
+    }
+
+    private void testTelemetryPushedAtoms(boolean protectedVm) throws Exception {
         // Reset statsd config and report before the test
         ConfigUtils.removeConfig(getDevice());
         ReportUtils.clearReports(getDevice());
@@ -682,6 +746,7 @@
                         .debugLevel("full")
                         .memoryMib(minMemorySize())
                         .cpuTopology("match_host")
+                        .protectedVm(protectedVm)
                         .build(device);
         microdroid.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
         device.shutdownMicrodroid(microdroid);
@@ -708,7 +773,7 @@
                 data.get(0).getAtom().getVmCreationRequested();
         assertThat(atomVmCreationRequested.getHypervisor())
                 .isEqualTo(AtomsProto.VmCreationRequested.Hypervisor.PKVM);
-        assertThat(atomVmCreationRequested.getIsProtected()).isFalse();
+        assertThat(atomVmCreationRequested.getIsProtected()).isEqualTo(protectedVm);
         assertThat(atomVmCreationRequested.getCreationSucceeded()).isTrue();
         assertThat(atomVmCreationRequested.getBinderExceptionCode()).isEqualTo(0);
         assertThat(atomVmCreationRequested.getVmIdentifier()).isEqualTo("VmRunApp");
@@ -737,7 +802,19 @@
 
     @Test
     @CddTest(requirements = {"9.17/C-1-1", "9.17/C-1-2", "9.17/C/1-3"})
-    public void testMicrodroidBoots() throws Exception {
+    public void testMicrodroidBootsOnPvm() throws Exception {
+        assumeProtectedVmSupported();
+        testMicrodroidBoots(true);
+    }
+
+    @Test
+    @CddTest(requirements = {"9.17/C-1-1", "9.17/C-1-2", "9.17/C/1-3"})
+    public void testMicrodroidBootsOnNonPvm() throws Exception {
+        assumeNonProtectedVmSupported();
+        testMicrodroidBoots(false);
+    }
+
+    private void testMicrodroidBoots(boolean protectedVm) throws Exception {
         CommandRunner android = new CommandRunner(getDevice());
 
         final String configPath = "assets/vm_config.json"; // path inside the APK
@@ -746,6 +823,7 @@
                         .debugLevel("full")
                         .memoryMib(minMemorySize())
                         .cpuTopology("match_host")
+                        .protectedVm(protectedVm)
                         .build(getAndroidDevice());
         mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
         CommandRunner microdroid = new CommandRunner(mMicrodroidDevice);
@@ -804,13 +882,25 @@
     }
 
     @Test
-    public void testMicrodroidRamUsage() throws Exception {
+    public void testMicrodroidRamUsageOnPvm() throws Exception {
+        assumeProtectedVmSupported();
+        testMicrodroidRamUsage(true);
+    }
+
+    @Test
+    public void testMicrodroidRamUsageOnNonPvm() throws Exception {
+        assumeNonProtectedVmSupported();
+        testMicrodroidRamUsage(false);
+    }
+
+    private void testMicrodroidRamUsage(boolean protectedVm) throws Exception {
         final String configPath = "assets/vm_config.json";
         mMicrodroidDevice =
                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
                         .debugLevel("full")
                         .memoryMib(minMemorySize())
                         .cpuTopology("match_host")
+                        .protectedVm(protectedVm)
                         .build(getAndroidDevice());
         mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
         mMicrodroidDevice.enableAdbRoot();
@@ -960,9 +1050,6 @@
         prepareVirtualizationTestSetup(getDevice());
 
         getDevice().installPackage(findTestFile(APK_NAME), /* reinstall */ false);
-
-        // clear the log
-        getDevice().executeShellV2Command("logcat -c");
     }
 
     @After
@@ -987,6 +1074,18 @@
                         "android.permission.USE_CUSTOM_VIRTUAL_MACHINE");
     }
 
+    private void assumeProtectedVmSupported() throws Exception {
+        assumeTrue(
+                "Test skipped because protected VMs are not supported",
+                getAndroidDevice().supportsMicrodroid(true));
+    }
+
+    private void assumeNonProtectedVmSupported() throws Exception {
+        assumeTrue(
+                "Test skipped because non-protected VMs are not supported",
+                getAndroidDevice().supportsMicrodroid(false));
+    }
+
     private TestDevice getAndroidDevice() {
         TestDevice androidDevice = (TestDevice) getDevice();
         assertThat(androidDevice).isNotNull();
diff --git a/tests/testapk/Android.bp b/tests/testapk/Android.bp
index fe8f5c9..8a31c21 100644
--- a/tests/testapk/Android.bp
+++ b/tests/testapk/Android.bp
@@ -43,7 +43,10 @@
     ],
     min_sdk_version: "33",
     // Defined in ../vmshareapp/Android.bp
-    data: [":MicrodroidVmShareApp"],
+    data: [
+        ":MicrodroidVmShareApp",
+        ":test_microdroid_vendor_image",
+    ],
 }
 
 // Defaults shared between MicrodroidTestNativeLib and MicrodroidPayloadInOtherAppNativeLib shared
diff --git a/tests/testapk/AndroidManifest.xml b/tests/testapk/AndroidManifest.xml
index 2ea3f6c..d6e6004 100644
--- a/tests/testapk/AndroidManifest.xml
+++ b/tests/testapk/AndroidManifest.xml
@@ -22,8 +22,7 @@
     <queries>
         <package android:name="com.android.microdroid.vmshare_app" />
     </queries>
-    <application>
-    </application>
+    <application />
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
         android:targetPackage="com.android.microdroid.test"
         android:label="Microdroid Test" />
diff --git a/tests/testapk/AndroidTest.xml b/tests/testapk/AndroidTest.xml
index 929dd31..e72a2e3 100644
--- a/tests/testapk/AndroidTest.xml
+++ b/tests/testapk/AndroidTest.xml
@@ -23,6 +23,14 @@
         <option name="test-file-name" value="MicrodroidTestApp.apk" />
         <option name="test-file-name" value="MicrodroidVmShareApp.apk" />
     </target_preparer>
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        <option name="run-command" value="mkdir -p /data/local/tmp/cts/microdroid" />
+        <option name="teardown-command" value="rm -rf /data/local/tmp/cts/microdroid" />
+    </target_preparer>
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
+        <option name="cleanup" value="true" />
+        <option name="push" value="test_microdroid_vendor_image.img->/data/local/tmp/cts/microdroid/test_microdroid_vendor_image.img" />
+    </target_preparer>
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="com.android.microdroid.test" />
         <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
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 a64b62a..f6dc1b8 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -128,6 +128,13 @@
     public void setup() {
         grantPermission(VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION);
         prepareTestSetup(mProtectedVm);
+        // USE_CUSTOM_VIRTUAL_MACHINE permission has protection level signature|development, meaning
+        // that it will be automatically granted when test apk is installed. We have some tests
+        // checking the behavior when caller doesn't have this permission (e.g.
+        // createVmWithConfigRequiresPermission). Proactively revoke the permission so that such
+        // tests can pass when ran by itself, e.g.:
+        // atest com.android.microdroid.test.MicrodroidTests#createVmWithConfigRequiresPermission
+        revokePermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
     }
 
     @After
@@ -548,6 +555,14 @@
                         .setVmOutputCaptured(true);
         e = assertThrows(IllegalStateException.class, () -> captureOutputOnNonDebuggable.build());
         assertThat(e).hasMessageThat().contains("debug level must be FULL to capture output");
+
+        VirtualMachineConfig.Builder captureInputOnNonDebuggable =
+                newVmConfigBuilder()
+                        .setPayloadBinaryName("binary.so")
+                        .setDebugLevel(VirtualMachineConfig.DEBUG_LEVEL_NONE)
+                        .setVmConsoleInputSupported(true);
+        e = assertThrows(IllegalStateException.class, () -> captureInputOnNonDebuggable.build());
+        assertThat(e).hasMessageThat().contains("debug level must be FULL to use console input");
     }
 
     @Test
@@ -586,6 +601,9 @@
                 newBaselineBuilder().setDebugLevel(DEBUG_LEVEL_FULL);
         VirtualMachineConfig debuggable = debuggableBuilder.build();
         assertConfigCompatible(debuggable, debuggableBuilder.setVmOutputCaptured(true)).isFalse();
+        assertConfigCompatible(debuggable, debuggableBuilder.setVmOutputCaptured(false)).isTrue();
+        assertConfigCompatible(debuggable, debuggableBuilder.setVmConsoleInputSupported(true))
+                .isFalse();
 
         VirtualMachineConfig currentContextConfig =
                 new VirtualMachineConfig.Builder(getContext())
@@ -1575,6 +1593,7 @@
                         .setProtectedVm(mProtectedVm)
                         .setPayloadBinaryName("MicrodroidTestNativeLib.so")
                         .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .setVmConsoleInputSupported(true) // even if console input is supported
                         .build();
         final VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_forward_log", vmConfig);
         vm.run();
@@ -1589,6 +1608,28 @@
         }
     }
 
+    @Test
+    public void inputShouldBeExplicitlyAllowed() throws Exception {
+        assumeSupportedDevice();
+
+        final VirtualMachineConfig vmConfig =
+                new VirtualMachineConfig.Builder(getContext())
+                        .setProtectedVm(mProtectedVm)
+                        .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .setVmOutputCaptured(true) // even if output is captured
+                        .build();
+        final VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_forward_log", vmConfig);
+        vm.run();
+
+        try {
+            assertThrowsVmExceptionContaining(
+                    () -> vm.getConsoleInput(), "VM console input is not supported");
+        } finally {
+            vm.stop();
+        }
+    }
+
     private boolean checkVmOutputIsRedirectedToLogcat(boolean debuggable) throws Exception {
         String time =
                 LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"));
@@ -1652,6 +1693,35 @@
     }
 
     @Test
+    public void testConsoleInputSupported() throws Exception {
+        assumeSupportedDevice();
+
+        VirtualMachineConfig config =
+                newVmConfigBuilder()
+                        .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .setVmConsoleInputSupported(true)
+                        .setVmOutputCaptured(true)
+                        .build();
+        VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_console_in", config);
+
+        final String TYPED = "this is a console input\n";
+        TestResults testResults =
+                runVmTestService(
+                        TAG,
+                        vm,
+                        (ts, tr) -> {
+                            OutputStreamWriter consoleIn =
+                                    new OutputStreamWriter(vm.getConsoleInput());
+                            consoleIn.write(TYPED);
+                            consoleIn.close();
+                            tr.mConsoleInput = ts.readLineFromConsole();
+                        });
+        testResults.assertNoException();
+        assertThat(testResults.mConsoleInput).isEqualTo(TYPED);
+    }
+
+    @Test
     public void testStartVmWithPayloadOfAnotherApp() throws Exception {
         assumeSupportedDevice();
 
@@ -1905,8 +1975,10 @@
                 .isEqualTo(OsConstants.S_IRUSR | OsConstants.S_IXUSR);
     }
 
-    // Taken from bionic/libs/kernel/uapi/linux/mounth.h.
+    // Taken from bionic/libc/kernel/uapi/linux/mount.h
+    private static final int MS_RDONLY = 1;
     private static final int MS_NOEXEC = 8;
+    private static final int MS_NOATIME = 1024;
 
     @Test
     @CddTest(requirements = {"9.17/C-1-5"})
@@ -1980,6 +2052,85 @@
         }
     }
 
+    @Test
+    public void configuringVendorDiskImageRequiresCustomPermission() throws Exception {
+        assumeSupportedDevice();
+
+        File vendorDiskImage =
+                new File("/data/local/tmp/cts/microdroid/test_microdroid_vendor_image.img");
+        VirtualMachineConfig config =
+                newVmConfigBuilder()
+                        .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+                        .setVendorDiskImage(vendorDiskImage)
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .build();
+
+        VirtualMachine vm =
+                forceCreateNewVirtualMachine("test_vendor_image_req_custom_permission", config);
+
+        SecurityException e =
+                assertThrows(
+                        SecurityException.class, () -> runVmTestService(TAG, vm, (ts, tr) -> {}));
+        assertThat(e)
+                .hasMessageThat()
+                .contains("android.permission.USE_CUSTOM_VIRTUAL_MACHINE permission");
+    }
+
+    @Test
+    public void bootsWithVendorPartition() throws Exception {
+        assumeSupportedDevice();
+
+        grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
+
+        File vendorDiskImage =
+                new File("/data/local/tmp/cts/microdroid/test_microdroid_vendor_image.img");
+        VirtualMachineConfig config =
+                newVmConfigBuilder()
+                        .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+                        .setVendorDiskImage(vendorDiskImage)
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .build();
+
+        VirtualMachine vm = forceCreateNewVirtualMachine("test_boot_with_vendor", config);
+
+        TestResults testResults =
+                runVmTestService(
+                        TAG,
+                        vm,
+                        (ts, tr) -> {
+                            tr.mMountFlags = ts.getMountFlags("/vendor");
+                        });
+
+        assertThat(testResults.mException).isNull();
+        int expectedFlags = MS_NOATIME | MS_RDONLY;
+        assertThat(testResults.mMountFlags & expectedFlags).isEqualTo(expectedFlags);
+    }
+
+    @Test
+    public void systemPartitionMountFlags() throws Exception {
+        assumeSupportedDevice();
+
+        VirtualMachineConfig config =
+                newVmConfigBuilder()
+                        .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .build();
+
+        VirtualMachine vm = forceCreateNewVirtualMachine("test_system_mount_flags", config);
+
+        TestResults testResults =
+                runVmTestService(
+                        TAG,
+                        vm,
+                        (ts, tr) -> {
+                            tr.mMountFlags = ts.getMountFlags("/");
+                        });
+
+        assertThat(testResults.mException).isNull();
+        int expectedFlags = MS_NOATIME | MS_RDONLY;
+        assertThat(testResults.mMountFlags & expectedFlags).isEqualTo(expectedFlags);
+    }
+
     private static class VmShareServiceConnection implements ServiceConnection {
 
         private final CountDownLatch mLatch = new CountDownLatch(1);
diff --git a/tests/testapk/src/native/testbinary.cpp b/tests/testapk/src/native/testbinary.cpp
index 7e0fc5b..297b505 100644
--- a/tests/testapk/src/native/testbinary.cpp
+++ b/tests/testapk/src/native/testbinary.cpp
@@ -313,6 +313,26 @@
             return ScopedAStatus::ok();
         }
 
+        ScopedAStatus readLineFromConsole(std::string* out) {
+            FILE* f = fopen("/dev/console", "r");
+            if (f == nullptr) {
+                return ScopedAStatus::fromExceptionCodeWithMessage(EX_SERVICE_SPECIFIC,
+                                                                   "failed to open /dev/console");
+            }
+            char* line = nullptr;
+            size_t len = 0;
+            ssize_t nread = getline(&line, &len, f);
+
+            if (nread == -1) {
+                free(line);
+                return ScopedAStatus::fromExceptionCodeWithMessage(EX_SERVICE_SPECIFIC,
+                                                                   "failed to read /dev/console");
+            }
+            out->append(line, nread);
+            free(line);
+            return ScopedAStatus::ok();
+        }
+
         ScopedAStatus quit() override { exit(0); }
     };
     auto testService = ndk::SharedRefBase::make<TestService>();
diff --git a/tests/vendor_images/Android.bp b/tests/vendor_images/Android.bp
new file mode 100644
index 0000000..09c657c
--- /dev/null
+++ b/tests/vendor_images/Android.bp
@@ -0,0 +1,9 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_filesystem {
+    name: "test_microdroid_vendor_image",
+    type: "ext4",
+    file_contexts: ":microdroid_vendor_file_contexts.gen",
+}
diff --git a/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java b/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java
index 1f7ffb7..0ddf70b 100644
--- a/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java
+++ b/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java
@@ -245,6 +245,11 @@
         }
 
         @Override
+        public String readLineFromConsole() {
+            throw new UnsupportedOperationException("Not supported");
+        }
+
+        @Override
         public void quit() throws RemoteException {
             throw new UnsupportedOperationException("Not supported");
         }
diff --git a/virtualizationmanager/src/aidl.rs b/virtualizationmanager/src/aidl.rs
index 968d2d2..b2497b1 100644
--- a/virtualizationmanager/src/aidl.rs
+++ b/virtualizationmanager/src/aidl.rs
@@ -20,7 +20,7 @@
 use crate::composite::make_composite_image;
 use crate::crosvm::{CrosvmConfig, DiskFile, PayloadState, VmContext, VmInstance, VmState};
 use crate::debug_config::DebugConfig;
-use crate::payload::{add_microdroid_payload_images, add_microdroid_system_images};
+use crate::payload::{add_microdroid_payload_images, add_microdroid_system_images, add_microdroid_vendor_image};
 use crate::selinux::{getfilecon, SeContext};
 use android_os_permissions_aidl::aidl::android::os::IPermissionController;
 use android_system_virtualizationcommon::aidl::android::system::virtualizationcommon::{
@@ -186,11 +186,18 @@
     fn createVm(
         &self,
         config: &VirtualMachineConfig,
-        console_fd: Option<&ParcelFileDescriptor>,
+        console_out_fd: Option<&ParcelFileDescriptor>,
+        console_in_fd: Option<&ParcelFileDescriptor>,
         log_fd: Option<&ParcelFileDescriptor>,
     ) -> binder::Result<Strong<dyn IVirtualMachine>> {
         let mut is_protected = false;
-        let ret = self.create_vm_internal(config, console_fd, log_fd, &mut is_protected);
+        let ret = self.create_vm_internal(
+            config,
+            console_out_fd,
+            console_in_fd,
+            log_fd,
+            &mut is_protected,
+        );
         write_vm_creation_stats(config, is_protected, &ret);
         ret
     }
@@ -303,7 +310,8 @@
     fn create_vm_internal(
         &self,
         config: &VirtualMachineConfig,
-        console_fd: Option<&ParcelFileDescriptor>,
+        console_out_fd: Option<&ParcelFileDescriptor>,
+        console_in_fd: Option<&ParcelFileDescriptor>,
         log_fd: Option<&ParcelFileDescriptor>,
         is_protected: &mut bool,
     ) -> binder::Result<Strong<dyn IVirtualMachine>> {
@@ -350,8 +358,9 @@
         };
 
         let state = &mut *self.state.lock().unwrap();
-        let console_fd =
-            clone_or_prepare_logger_fd(&debug_config, console_fd, format!("Console({})", cid))?;
+        let console_out_fd =
+            clone_or_prepare_logger_fd(&debug_config, console_out_fd, format!("Console({})", cid))?;
+        let console_in_fd = console_in_fd.map(clone_file).transpose()?;
         let log_fd = clone_or_prepare_logger_fd(&debug_config, log_fd, format!("Log({})", cid))?;
 
         // Counter to generate unique IDs for temporary image files.
@@ -455,7 +464,8 @@
             cpus,
             host_cpu_topology,
             task_profiles: config.taskProfiles.clone(),
-            console_fd,
+            console_out_fd,
+            console_in_fd,
             log_fd,
             ramdump,
             indirect_files,
@@ -569,6 +579,15 @@
     Ok(DiskFile { image, writable: disk.writable })
 }
 
+fn append_kernel_param(param: &str, vm_config: &mut VirtualMachineRawConfig) {
+    if let Some(ref mut params) = vm_config.params {
+        params.push(' ');
+        params.push_str(param)
+    } else {
+        vm_config.params = Some(param.to_owned())
+    }
+}
+
 fn load_app_config(
     config: &VirtualMachineAppConfig,
     debug_config: &DebugConfig,
@@ -610,6 +629,11 @@
         }
         vm_config.taskProfiles = custom_config.taskProfiles.clone();
         vm_config.gdbPort = custom_config.gdbPort;
+
+        if let Some(file) = custom_config.vendorImage.as_ref() {
+            add_microdroid_vendor_image(clone_file(file)?, &mut vm_config);
+            append_kernel_param("androidboot.microdroid.mount_vendor=1", &mut vm_config)
+        }
     }
 
     if config.memoryMib > 0 {
@@ -1076,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 {
@@ -1339,4 +1364,19 @@
         assert!(modified_orig == modified_new, "idsig file was updated unnecessarily");
         Ok(())
     }
+
+    #[test]
+    fn test_append_kernel_param_first_param() {
+        let mut vm_config = VirtualMachineRawConfig { ..Default::default() };
+        append_kernel_param("foo=1", &mut vm_config);
+        assert_eq!(vm_config.params, Some("foo=1".to_owned()))
+    }
+
+    #[test]
+    fn test_append_kernel_param() {
+        let mut vm_config =
+            VirtualMachineRawConfig { params: Some("foo=5".to_owned()), ..Default::default() };
+        append_kernel_param("bar=42", &mut vm_config);
+        assert_eq!(vm_config.params, Some("foo=5 bar=42".to_owned()))
+    }
 }
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 856ff1e..31db3f6 100644
--- a/virtualizationmanager/src/crosvm.rs
+++ b/virtualizationmanager/src/crosvm.rs
@@ -107,7 +107,8 @@
     pub cpus: Option<NonZeroU32>,
     pub host_cpu_topology: bool,
     pub task_profiles: Vec<String>,
-    pub console_fd: Option<File>,
+    pub console_out_fd: Option<File>,
+    pub console_in_fd: Option<File>,
     pub log_fd: Option<File>,
     pub ramdump: Option<File>,
     pub indirect_files: Vec<File>,
@@ -526,7 +527,7 @@
                 }
             }
             Ok(VmResponse::Err(e)) => {
-                // ENOTSUP is returned when the balloon protocol is not initialised. This
+                // ENOTSUP is returned when the balloon protocol is not initialized. This
                 // can occur for numerous reasons: Guest is still booting, guest doesn't
                 // support ballooning, host doesn't support ballooning. We don't log or
                 // raise an error in this case: trim is just a hint and we can ignore it.
@@ -591,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)
 }
@@ -776,21 +777,29 @@
     //
     // When [console|log]_fd is not specified, the devices are attached to sink, which means what's
     // written there is discarded.
-    let console_arg = format_serial_arg(&mut preserved_fds, &config.console_fd);
-    let log_arg = format_serial_arg(&mut preserved_fds, &config.log_fd);
+    let console_out_arg = format_serial_out_arg(&mut preserved_fds, &config.console_out_fd);
+    let console_in_arg = config
+        .console_in_fd
+        .as_ref()
+        .map(|fd| format!(",input={}", add_preserved_fd(&mut preserved_fds, fd)))
+        .unwrap_or_default();
+    let log_arg = format_serial_out_arg(&mut preserved_fds, &config.log_fd);
     let failure_serial_path = add_preserved_fd(&mut preserved_fds, &failure_pipe_write);
-    let ramdump_arg = format_serial_arg(&mut preserved_fds, &config.ramdump);
+    let ramdump_arg = format_serial_out_arg(&mut preserved_fds, &config.ramdump);
 
     // Warning: Adding more serial devices requires you to shift the PCI device ID of the boot
     // disks in bootconfig.x86_64. This is because x86 crosvm puts serial devices and the block
     // devices in the same PCI bus and serial devices comes before the block devices. Arm crosvm
     // doesn't have the issue.
     // /dev/ttyS0
-    command.arg(format!("--serial={},hardware=serial,num=1", &console_arg));
+    command.arg(format!("--serial={},hardware=serial,num=1", &console_out_arg));
     // /dev/ttyS1
     command.arg(format!("--serial=type=file,path={},hardware=serial,num=2", &failure_serial_path));
     // /dev/hvc0
-    command.arg(format!("--serial={},hardware=virtio-console,num=1", &console_arg));
+    command.arg(format!(
+        "--serial={}{},hardware=virtio-console,num=1",
+        &console_out_arg, &console_in_arg
+    ));
     // /dev/hvc1
     command.arg(format!("--serial={},hardware=virtio-console,num=2", &ramdump_arg));
     // /dev/hvc2
@@ -890,7 +899,7 @@
 
 /// Adds the file descriptor for `file` (if any) to `preserved_fds`, and returns the appropriate
 /// string for a crosvm `--serial` flag. If `file` is none, creates a dummy sink device.
-fn format_serial_arg(preserved_fds: &mut Vec<RawFd>, file: &Option<File>) -> String {
+fn format_serial_out_arg(preserved_fds: &mut Vec<RawFd>, file: &Option<File>) -> String {
     if let Some(file) = file {
         format!("type=file,path={}", add_preserved_fd(preserved_fds, file))
     } else {
@@ -901,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/virtualizationmanager/src/payload.rs b/virtualizationmanager/src/payload.rs
index 33659d4..ab6f31c 100644
--- a/virtualizationmanager/src/payload.rs
+++ b/virtualizationmanager/src/payload.rs
@@ -194,12 +194,12 @@
     temporary_directory: &Path,
 ) -> Result<ParcelFileDescriptor> {
     let payload_metadata = match &app_config.payload {
-        Payload::PayloadConfig(payload_config) => PayloadMetadata::config(PayloadConfig {
+        Payload::PayloadConfig(payload_config) => PayloadMetadata::Config(PayloadConfig {
             payload_binary_name: payload_config.payloadBinaryName.clone(),
             ..Default::default()
         }),
         Payload::ConfigPath(config_path) => {
-            PayloadMetadata::config_path(format!("/mnt/apk/{}", config_path))
+            PayloadMetadata::ConfigPath(format!("/mnt/apk/{}", config_path))
         }
     };
 
@@ -398,6 +398,18 @@
         .collect()
 }
 
+pub fn add_microdroid_vendor_image(vendor_image: File, vm_config: &mut VirtualMachineRawConfig) {
+    vm_config.disks.push(DiskImage {
+        image: None,
+        writable: false,
+        partitions: vec![Partition {
+            label: "microdroid-vendor".to_owned(),
+            image: Some(ParcelFileDescriptor::new(vendor_image)),
+            writable: false,
+        }],
+    })
+}
+
 pub fn add_microdroid_system_images(
     config: &VirtualMachineAppConfig,
     instance_file: File,
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
index d72d5ac..99bc076 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
@@ -23,12 +23,14 @@
 interface IVirtualizationService {
     /**
      * Create the VM with the given config file, and return a handle to it ready to start it. If
-     * `consoleFd` is provided then console output from the VM will be sent to it. If `osLogFd` is
+     * `consoleOutFd` is provided then console output from the VM will be sent to it. If
+     * `consoleInFd` is provided then console input to the VM will be read from it. If `osLogFd` is
      * provided then the OS-level logs will be sent to it. `osLogFd` is supported only when the OS
      * running in the VM has the logging system. In case of Microdroid, the logging system is logd.
      */
     IVirtualMachine createVm(in VirtualMachineConfig config,
-            in @nullable ParcelFileDescriptor consoleFd,
+            in @nullable ParcelFileDescriptor consoleOutFd,
+            in @nullable ParcelFileDescriptor consoleInFd,
             in @nullable ParcelFileDescriptor osLogFd);
 
     /**
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
index 6a0bf7c..2b762c4 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
@@ -105,6 +105,9 @@
          * List of task profile names to apply for the VM
          */
         String[] taskProfiles;
+
+        /** A disk image containing vendor specific modules. */
+        @nullable ParcelFileDescriptor vendorImage;
     }
 
     /** Configuration parameters guarded by android.permission.USE_CUSTOM_VIRTUAL_MACHINE */
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/virtualizationservice/src/rkpvm.rs b/virtualizationservice/src/rkpvm.rs
index a4649f6..ebfb667 100644
--- a/virtualizationservice/src/rkpvm.rs
+++ b/virtualizationservice/src/rkpvm.rs
@@ -79,7 +79,7 @@
         taskProfiles: vec![],
         gdbPort: 0, // No gdb
     });
-    let vm = VmInstance::create(service.as_ref(), &config, None, None, None)
+    let vm = VmInstance::create(service.as_ref(), &config, None, None, None, None)
         .context("Failed to create service VM")?;
 
     info!("service_vm: Starting Service VM...");
diff --git a/vm/src/main.rs b/vm/src/main.rs
index bc3f4da..0c99acb 100644
--- a/vm/src/main.rs
+++ b/vm/src/main.rs
@@ -74,12 +74,16 @@
         #[clap(long)]
         console: Option<PathBuf>,
 
+        /// Path to file for VM console input.
+        #[clap(long)]
+        console_in: Option<PathBuf>,
+
         /// Path to file for VM log output.
         #[clap(long)]
         log: Option<PathBuf>,
 
-        /// Debug level of the VM. Supported values: "none" (default), and "full".
-        #[clap(long, default_value = "none", value_parser = parse_debug_level)]
+        /// Debug level of the VM. Supported values: "full" (default), and "none".
+        #[clap(long, default_value = "full", value_parser = parse_debug_level)]
         debug: DebugLevel,
 
         /// Run VM in protected mode.
@@ -111,6 +115,10 @@
         /// Path to custom kernel image to use when booting Microdroid.
         #[clap(long)]
         kernel: Option<PathBuf>,
+
+        /// Path to disk image containing vendor-specific modules.
+        #[clap(long)]
+        vendor: Option<PathBuf>,
     },
     /// Run a virtual machine with Microdroid inside
     RunMicrodroid {
@@ -138,11 +146,15 @@
         #[clap(long)]
         console: Option<PathBuf>,
 
+        /// Path to file for VM console input.
+        #[clap(long)]
+        console_in: Option<PathBuf>,
+
         /// Path to file for VM log output.
         #[clap(long)]
         log: Option<PathBuf>,
 
-        /// Debug level of the VM. Supported values: "none" (default), and "full".
+        /// Debug level of the VM. Supported values: "full" (default), and "none".
         #[clap(long, default_value = "full", value_parser = parse_debug_level)]
         debug: DebugLevel,
 
@@ -171,6 +183,10 @@
         /// Path to custom kernel image to use when booting Microdroid.
         #[clap(long)]
         kernel: Option<PathBuf>,
+
+        /// Path to disk image containing vendor-specific modules.
+        #[clap(long)]
+        vendor: Option<PathBuf>,
     },
     /// Run a virtual machine
     Run {
@@ -193,6 +209,10 @@
         #[clap(long)]
         console: Option<PathBuf>,
 
+        /// Path to file for VM console input.
+        #[clap(long)]
+        console_in: Option<PathBuf>,
+
         /// Path to file for VM log output.
         #[clap(long)]
         log: Option<PathBuf>,
@@ -277,6 +297,7 @@
             config_path,
             payload_binary_name,
             console,
+            console_in,
             log,
             debug,
             protected,
@@ -286,6 +307,7 @@
             extra_idsigs,
             gdb,
             kernel,
+            vendor,
         } => command_run_app(
             name,
             get_service()?.as_ref(),
@@ -297,6 +319,7 @@
             config_path,
             payload_binary_name,
             console.as_deref(),
+            console_in.as_deref(),
             log.as_deref(),
             debug,
             protected,
@@ -306,6 +329,7 @@
             &extra_idsigs,
             gdb,
             kernel.as_deref(),
+            vendor.as_deref(),
         ),
         Opt::RunMicrodroid {
             name,
@@ -313,6 +337,7 @@
             storage,
             storage_size,
             console,
+            console_in,
             log,
             debug,
             protected,
@@ -321,6 +346,7 @@
             task_profiles,
             gdb,
             kernel,
+            vendor,
         } => command_run_microdroid(
             name,
             get_service()?.as_ref(),
@@ -328,6 +354,7 @@
             storage.as_deref(),
             storage_size,
             console.as_deref(),
+            console_in.as_deref(),
             log.as_deref(),
             debug,
             protected,
@@ -336,13 +363,15 @@
             task_profiles,
             gdb,
             kernel.as_deref(),
+            vendor.as_deref(),
         ),
-        Opt::Run { name, config, cpu_topology, task_profiles, console, log, gdb } => {
+        Opt::Run { name, config, cpu_topology, task_profiles, console, console_in, log, gdb } => {
             command_run(
                 name,
                 get_service()?.as_ref(),
                 &config,
                 console.as_deref(),
+                console_in.as_deref(),
                 log.as_deref(),
                 /* mem */ None,
                 cpu_topology,
diff --git a/vm/src/run.rs b/vm/src/run.rs
index 392fa1c..f50bd50 100644
--- a/vm/src/run.rs
+++ b/vm/src/run.rs
@@ -54,7 +54,8 @@
     storage_size: Option<u64>,
     config_path: Option<String>,
     payload_binary_name: Option<String>,
-    console_path: Option<&Path>,
+    console_out_path: Option<&Path>,
+    console_in_path: Option<&Path>,
     log_path: Option<&Path>,
     debug_level: DebugLevel,
     protected: bool,
@@ -64,6 +65,7 @@
     extra_idsigs: &[PathBuf],
     gdb: Option<NonZeroU16>,
     kernel: Option<&Path>,
+    vendor: Option<&Path>,
 ) -> Result<(), Error> {
     let apk_file = File::open(apk).context("Failed to open APK file")?;
 
@@ -121,6 +123,8 @@
 
     let kernel = kernel.map(|p| open_parcel_file(p, false)).transpose()?;
 
+    let vendor = vendor.map(|p| open_parcel_file(p, false)).transpose()?;
+
     let extra_idsig_files: Result<Vec<File>, _> = extra_idsigs.iter().map(File::open).collect();
     let extra_idsig_fds = extra_idsig_files?.into_iter().map(ParcelFileDescriptor::new).collect();
 
@@ -143,6 +147,7 @@
         customKernelImage: kernel,
         gdbPort: gdb.map(u16::from).unwrap_or(0) as i32, // 0 means no gdb
         taskProfiles: task_profiles,
+        vendorImage: vendor,
     };
 
     let config = VirtualMachineConfig::AppConfig(VirtualMachineAppConfig {
@@ -159,7 +164,7 @@
         cpuTopology: cpu_topology,
         customConfig: Some(custom_config),
     });
-    run(service, &config, &payload_config_str, console_path, log_path)
+    run(service, &config, &payload_config_str, console_out_path, console_in_path, log_path)
 }
 
 fn find_empty_payload_apk_path() -> Result<PathBuf, Error> {
@@ -192,7 +197,8 @@
     work_dir: Option<PathBuf>,
     storage: Option<&Path>,
     storage_size: Option<u64>,
-    console_path: Option<&Path>,
+    console_out_path: Option<&Path>,
+    console_in_path: Option<&Path>,
     log_path: Option<&Path>,
     debug_level: DebugLevel,
     protected: bool,
@@ -201,6 +207,7 @@
     task_profiles: Vec<String>,
     gdb: Option<NonZeroU16>,
     kernel: Option<&Path>,
+    vendor: Option<&Path>,
 ) -> Result<(), Error> {
     let apk = find_empty_payload_apk_path()?;
     println!("found path {}", apk.display());
@@ -223,7 +230,8 @@
         storage_size,
         /* config_path= */ None,
         Some(payload_binary_name.to_owned()),
-        console_path,
+        console_out_path,
+        console_in_path,
         log_path,
         debug_level,
         protected,
@@ -233,6 +241,7 @@
         &extra_sig,
         gdb,
         kernel,
+        vendor,
     )
 }
 
@@ -242,7 +251,8 @@
     name: Option<String>,
     service: &dyn IVirtualizationService,
     config_path: &Path,
-    console_path: Option<&Path>,
+    console_out_path: Option<&Path>,
+    console_in_path: Option<&Path>,
     log_path: Option<&Path>,
     mem: Option<u32>,
     cpu_topology: CpuTopology,
@@ -269,7 +279,8 @@
         service,
         &VirtualMachineConfig::RawConfig(config),
         &format!("{:?}", config_path),
-        console_path,
+        console_out_path,
+        console_in_path,
         log_path,
     )
 }
@@ -290,28 +301,35 @@
     service: &dyn IVirtualizationService,
     config: &VirtualMachineConfig,
     payload_config: &str,
-    console_path: Option<&Path>,
+    console_out_path: Option<&Path>,
+    console_in_path: Option<&Path>,
     log_path: Option<&Path>,
 ) -> Result<(), Error> {
-    let console = if let Some(console_path) = console_path {
-        Some(
-            File::create(console_path)
-                .with_context(|| format!("Failed to open console file {:?}", console_path))?,
-        )
+    let console_out = if let Some(console_out_path) = console_out_path {
+        Some(File::create(console_out_path).with_context(|| {
+            format!("Failed to open console output file {:?}", console_out_path)
+        })?)
     } else {
-        Some(duplicate_stdout()?)
+        Some(duplicate_fd(io::stdout())?)
     };
+    let console_in =
+        if let Some(console_in_path) = console_in_path {
+            Some(File::create(console_in_path).with_context(|| {
+                format!("Failed to open console input file {:?}", console_in_path)
+            })?)
+        } else {
+            Some(duplicate_fd(io::stdin())?)
+        };
     let log = if let Some(log_path) = log_path {
         Some(
             File::create(log_path)
                 .with_context(|| format!("Failed to open log file {:?}", log_path))?,
         )
     } else {
-        Some(duplicate_stdout()?)
+        Some(duplicate_fd(io::stdout())?)
     };
-
     let callback = Box::new(Callback {});
-    let vm = VmInstance::create(service, config, console, log, Some(callback))
+    let vm = VmInstance::create(service, config, console_out, console_in, log, Some(callback))
         .context("Failed to create VM")?;
     vm.start().context("Failed to start VM")?;
 
@@ -361,17 +379,17 @@
     }
 }
 
-/// Safely duplicate the standard output file descriptor.
-fn duplicate_stdout() -> io::Result<File> {
-    let stdout_fd = io::stdout().as_raw_fd();
-    // Safe because this just duplicates a file descriptor which we know to be valid, and we check
-    // for an error.
-    let dup_fd = unsafe { libc::dup(stdout_fd) };
+/// Safely duplicate the file descriptor.
+fn duplicate_fd<T: AsRawFd>(file: T) -> io::Result<File> {
+    let fd = file.as_raw_fd();
+    // 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/README.md b/vmbase/README.md
index 7f621fb..280d7e1 100644
--- a/vmbase/README.md
+++ b/vmbase/README.md
@@ -6,7 +6,7 @@
 
 In particular it provides:
 
-- An [entry point](entry.S) that initialises the MMU with a hard-coded identity mapping, enables the
+- An [entry point](entry.S) that initializes the MMU with a hard-coded identity mapping, enables the
   cache, prepares the image and allocates a stack.
 - An [exception vector](exceptions.S) to call your exception handlers.
 - A UART driver and `println!` macro for early console logging.
@@ -62,7 +62,7 @@
 }
 ```
 
-vmbase adds a wrapper around your main function to initialise the console driver first (with the
+vmbase adds a wrapper around your main function to initialize the console driver first (with the
 UART at base address `0x3f8`, the first UART allocated by crosvm), and make a PSCI `SYSTEM_OFF` call
 to shutdown the VM if your main function ever returns.
 
@@ -93,7 +93,7 @@
 
 The `println!` macro shouldn't be used in exception handlers, because it relies on a global instance
 of the UART driver which might be locked when the exception happens, which would result in deadlock.
-Instead you can use `emergency_write_str` and `eprintln!`, which will re-initialise the UART every
+Instead you can use `emergency_write_str` and `eprintln!`, which will re-initialize the UART every
 time to ensure that it can be used. This should still be used with care, as it may interfere with
 whatever the rest of the program is doing with the UART.
 
diff --git a/vmbase/entry.S b/vmbase/entry.S
index 9f6993a..9177a4a 100644
--- a/vmbase/entry.S
+++ b/vmbase/entry.S
@@ -63,72 +63,6 @@
 .set .Lsctlrval, .L_SCTLR_ELx_M | .L_SCTLR_ELx_C | .L_SCTLR_ELx_SA | .L_SCTLR_EL1_ITD | .L_SCTLR_EL1_SED
 .set .Lsctlrval, .Lsctlrval | .L_SCTLR_ELx_I | .L_SCTLR_EL1_SPAN | .L_SCTLR_EL1_RES1 | .L_SCTLR_EL1_WXN
 
-/* SMC function IDs */
-.set .L_SMCCC_VERSION_ID, 0x80000000
-.set .L_SMCCC_TRNG_VERSION_ID, 0x84000050
-.set .L_SMCCC_TRNG_FEATURES_ID, 0x84000051
-.set .L_SMCCC_TRNG_RND64_ID, 0xc4000053
-
-/* SMC function versions */
-.set .L_SMCCC_VERSION_1_1, 0x0101
-.set .L_SMCCC_TRNG_VERSION_1_0, 0x0100
-
-/* Bionic-compatible stack protector */
-.section .data.stack_protector, "aw"
-__bionic_tls:
-	.zero	40
-.global __stack_chk_guard
-__stack_chk_guard:
-	.quad	0
-
-/**
- * This macro stores a random value into a register.
- * If a TRNG backed is not present or if an error occurs, the value remains unchanged.
- */
-.macro rnd_reg reg:req
-	mov x20, x0
-	mov x21, x1
-	mov x22, x2
-	mov x23, x3
-
-	/* Verify SMCCC version >=1.1 */
-	hvc_call .L_SMCCC_VERSION_ID
-	cmp w0, 0
-	b.lt 100f
-	cmp w0, .L_SMCCC_VERSION_1_1
-	b.lt 100f
-
-	/* Verify TRNG ABI version 1.x */
-	hvc_call .L_SMCCC_TRNG_VERSION_ID
-	cmp w0, 0
-	b.lt 100f
-	cmp w0, .L_SMCCC_TRNG_VERSION_1_0
-	b.lt 100f
-
-	/* Call TRNG_FEATURES, ensure TRNG_RND is implemented */
-	mov_i x1, .L_SMCCC_TRNG_RND64_ID
-	hvc_call .L_SMCCC_TRNG_FEATURES_ID
-	cmp w0, 0
-	b.lt 100f
-
-	/* Call TRNG_RND, request 64 bits of entropy */
-	mov x1, #64
-	hvc_call .L_SMCCC_TRNG_RND64_ID
-	cmp x0, 0
-	b.lt 100f
-
-	mov \reg, x3
-	b 101f
-
-100:
-	reset_or_hang
-101:
-	mov x0, x20
-	mov x1, x21
-	mov x2, x22
-	mov x3, x23
-.endm
-
 /**
  * This is a generic entry point for an image. It carries out the operations required to prepare the
  * loaded image to be run. Specifically, it zeroes the bss section using registers x25 and above,
@@ -222,18 +156,17 @@
 	adr x30, vector_table_el1
 	msr vbar_el1, x30
 
-	/* Set up Bionic-compatible thread-local storage. */
+	/*
+	 * Set up Bionic-compatible thread-local storage.
+	 *
+	 * Note that TPIDR_EL0 can't be configured from rust_entry because the
+	 * compiler will dereference it during function entry to access
+	 * __stack_chk_guard and Rust doesn't support LLVM's
+	 * __attribute__((no_stack_protector)).
+	 */
 	adr_l x30, __bionic_tls
 	msr tpidr_el0, x30
 
-	/* Randomize stack protector. */
-	rnd_reg x29
-	adr_l x30, __stack_chk_guard
-	str x29, [x30]
-
-	/* Write a null byte to the top of the stack guard to act as a string terminator. */
-	strb wzr, [x30]
-
 	/* Call into Rust code. */
 	bl rust_entry
 
diff --git a/vmbase/example/src/exceptions.rs b/vmbase/example/src/exceptions.rs
index 0522013..5d7768a 100644
--- a/vmbase/example/src/exceptions.rs
+++ b/vmbase/example/src/exceptions.rs
@@ -14,8 +14,7 @@
 
 //! Exception handlers.
 
-use core::arch::asm;
-use vmbase::{eprintln, power::reboot};
+use vmbase::{eprintln, power::reboot, read_sysreg};
 
 #[no_mangle]
 extern "C" fn sync_exception_current(_elr: u64, _spsr: u64) {
@@ -71,9 +70,6 @@
 
 #[inline]
 fn print_esr() {
-    let mut esr: u64;
-    unsafe {
-        asm!("mrs {esr}, esr_el1", esr = out(reg) esr);
-    }
+    let esr = read_sysreg!("esr_el1");
     eprintln!("esr={:#08x}", esr);
 }
diff --git a/vmbase/example/src/layout.rs b/vmbase/example/src/layout.rs
index 2e9d27a..fc578bc 100644
--- a/vmbase/example/src/layout.rs
+++ b/vmbase/example/src/layout.rs
@@ -15,80 +15,36 @@
 //! Memory layout.
 
 use aarch64_paging::paging::{MemoryRegion, VirtualAddress};
-use core::arch::asm;
 use core::ops::Range;
 use log::info;
 use vmbase::layout;
-use vmbase::STACK_CHK_GUARD;
 
 /// The first 1 GiB of memory are used for MMIO.
 pub const DEVICE_REGION: MemoryRegion = MemoryRegion::new(0, 0x40000000);
 
-fn into_va_range(r: Range<usize>) -> Range<VirtualAddress> {
-    VirtualAddress(r.start)..VirtualAddress(r.end)
-}
-
-/// Memory reserved for the DTB.
-pub fn dtb_range() -> Range<VirtualAddress> {
-    into_va_range(layout::dtb_range())
-}
-
-/// Executable code.
-pub fn text_range() -> Range<VirtualAddress> {
-    into_va_range(layout::text_range())
-}
-
-/// Read-only data.
-pub fn rodata_range() -> Range<VirtualAddress> {
-    into_va_range(layout::rodata_range())
-}
-
-/// Initialised writable data.
-pub fn data_range() -> Range<VirtualAddress> {
-    into_va_range(layout::data_range())
-}
-
-/// Zero-initialised writable data.
-pub fn bss_range() -> Range<VirtualAddress> {
-    into_va_range(layout::bss_range())
-}
-
 /// Writable data region for the stack.
 pub fn boot_stack_range() -> Range<VirtualAddress> {
     const PAGE_SIZE: usize = 4 << 10;
-    into_va_range(layout::stack_range(40 * PAGE_SIZE))
-}
-
-/// Writable data region for allocations.
-pub fn scratch_range() -> Range<VirtualAddress> {
-    into_va_range(layout::scratch_range())
-}
-
-fn data_load_address() -> VirtualAddress {
-    VirtualAddress(layout::data_load_address())
-}
-
-fn binary_end() -> VirtualAddress {
-    VirtualAddress(layout::binary_end())
+    layout::stack_range(40 * PAGE_SIZE)
 }
 
 pub fn print_addresses() {
-    let dtb = dtb_range();
+    let dtb = layout::dtb_range();
     info!("dtb:        {}..{} ({} bytes)", dtb.start, dtb.end, dtb.end - dtb.start);
-    let text = text_range();
+    let text = layout::text_range();
     info!("text:       {}..{} ({} bytes)", text.start, text.end, text.end - text.start);
-    let rodata = rodata_range();
+    let rodata = layout::rodata_range();
     info!("rodata:     {}..{} ({} bytes)", rodata.start, rodata.end, rodata.end - rodata.start);
-    info!("binary end: {}", binary_end());
-    let data = data_range();
+    info!("binary end: {}", layout::binary_end());
+    let data = layout::data_range();
     info!(
         "data:       {}..{} ({} bytes, loaded at {})",
         data.start,
         data.end,
         data.end - data.start,
-        data_load_address(),
+        layout::data_load_address(),
     );
-    let bss = bss_range();
+    let bss = layout::bss_range();
     info!("bss:        {}..{} ({} bytes)", bss.start, bss.end, bss.end - bss.start);
     let boot_stack = boot_stack_range();
     info!(
@@ -98,18 +54,3 @@
         boot_stack.end - boot_stack.start
     );
 }
-
-/// Bionic-compatible thread-local storage entry, at the given offset from TPIDR_EL0.
-pub fn bionic_tls(off: usize) -> u64 {
-    let mut base: usize;
-    unsafe {
-        asm!("mrs {base}, tpidr_el0", base = out(reg) base);
-        let ptr = (base + off) as *const u64;
-        *ptr
-    }
-}
-
-/// Value of __stack_chk_guard.
-pub fn stack_chk_guard() -> u64 {
-    *STACK_CHK_GUARD
-}
diff --git a/vmbase/example/src/main.rs b/vmbase/example/src/main.rs
index b3b5732..a6f3bfa 100644
--- a/vmbase/example/src/main.rs
+++ b/vmbase/example/src/main.rs
@@ -16,6 +16,8 @@
 
 #![no_main]
 #![no_std]
+#![deny(unsafe_op_in_unsafe_fn)]
+#![deny(clippy::undocumented_unsafe_blocks)]
 
 mod exceptions;
 mod layout;
@@ -23,31 +25,53 @@
 
 extern crate alloc;
 
-use crate::layout::{
-    bionic_tls, boot_stack_range, dtb_range, print_addresses, rodata_range, scratch_range,
-    stack_chk_guard, text_range, DEVICE_REGION,
-};
+use crate::layout::{boot_stack_range, print_addresses, DEVICE_REGION};
 use crate::pci::{check_pci, get_bar_region};
-use aarch64_paging::{idmap::IdMap, paging::Attributes};
+use aarch64_paging::paging::MemoryRegion;
+use aarch64_paging::MapError;
 use alloc::{vec, vec::Vec};
 use fdtpci::PciInfo;
 use libfdt::Fdt;
 use log::{debug, error, info, trace, warn, LevelFilter};
-use vmbase::{configure_heap, cstr, logger, main, memory::SIZE_64KB};
+use vmbase::{
+    bionic, configure_heap, cstr,
+    layout::{dtb_range, rodata_range, scratch_range, text_range},
+    linker, logger, main,
+    memory::{PageTable, SIZE_64KB},
+};
 
 static INITIALISED_DATA: [u32; 4] = [1, 2, 3, 4];
 static mut ZEROED_DATA: [u32; 10] = [0; 10];
 static mut MUTABLE_DATA: [u32; 4] = [1, 2, 3, 4];
 
-const ASID: usize = 1;
-const ROOT_LEVEL: usize = 1;
-
 main!(main);
 configure_heap!(SIZE_64KB);
 
+fn init_page_table(pci_bar_range: &MemoryRegion) -> Result<(), MapError> {
+    let mut page_table = PageTable::default();
+
+    page_table.map_device(&DEVICE_REGION)?;
+    page_table.map_code(&text_range().into())?;
+    page_table.map_rodata(&rodata_range().into())?;
+    page_table.map_data(&scratch_range().into())?;
+    page_table.map_data(&boot_stack_range().into())?;
+    page_table.map_rodata(&dtb_range().into())?;
+    page_table.map_device(pci_bar_range)?;
+
+    info!("Activating IdMap...");
+    // SAFETY: page_table duplicates the static mappings for everything that the Rust code is
+    // aware of so activating it shouldn't have any visible effect.
+    unsafe {
+        page_table.activate();
+    }
+    info!("Activated.");
+
+    Ok(())
+}
+
 /// Entry point for VM bootloader.
 pub fn main(arg0: u64, arg1: u64, arg2: u64, arg3: u64) {
-    logger::init(LevelFilter::Debug).unwrap();
+    log::set_max_level(LevelFilter::Debug);
 
     info!("Hello world");
     info!("x0={:#018x}, x1={:#018x}, x2={:#018x}, x3={:#018x}", arg0, arg1, arg2, arg3);
@@ -58,8 +82,9 @@
 
     info!("Checking FDT...");
     let fdt = dtb_range();
-    let fdt =
-        unsafe { core::slice::from_raw_parts_mut(fdt.start.0 as *mut u8, fdt.end.0 - fdt.start.0) };
+    let fdt_size = fdt.end.0 - fdt.start.0;
+    // SAFETY: The DTB range is valid, writable memory, and we don't construct any aliases to it.
+    let fdt = unsafe { core::slice::from_raw_parts_mut(fdt.start.0 as *mut u8, fdt_size) };
     let fdt = Fdt::from_mut_slice(fdt).unwrap();
     info!("FDT passed verification.");
     check_fdt(fdt);
@@ -71,72 +96,12 @@
 
     check_alloc();
 
-    let mut idmap = IdMap::new(ASID, ROOT_LEVEL);
-    idmap
-        .map_range(
-            &DEVICE_REGION,
-            Attributes::VALID | Attributes::DEVICE_NGNRE | Attributes::EXECUTE_NEVER,
-        )
-        .unwrap();
-    idmap
-        .map_range(
-            &text_range().into(),
-            Attributes::VALID | Attributes::NORMAL | Attributes::NON_GLOBAL | Attributes::READ_ONLY,
-        )
-        .unwrap();
-    idmap
-        .map_range(
-            &rodata_range().into(),
-            Attributes::VALID
-                | Attributes::NORMAL
-                | Attributes::NON_GLOBAL
-                | Attributes::READ_ONLY
-                | Attributes::EXECUTE_NEVER,
-        )
-        .unwrap();
-    idmap
-        .map_range(
-            &scratch_range().into(),
-            Attributes::VALID
-                | Attributes::NORMAL
-                | Attributes::NON_GLOBAL
-                | Attributes::EXECUTE_NEVER,
-        )
-        .unwrap();
-    idmap
-        .map_range(
-            &boot_stack_range().into(),
-            Attributes::VALID
-                | Attributes::NORMAL
-                | Attributes::NON_GLOBAL
-                | Attributes::EXECUTE_NEVER,
-        )
-        .unwrap();
-    idmap
-        .map_range(
-            &dtb_range().into(),
-            Attributes::VALID
-                | Attributes::NORMAL
-                | Attributes::NON_GLOBAL
-                | Attributes::READ_ONLY
-                | Attributes::EXECUTE_NEVER,
-        )
-        .unwrap();
-    idmap
-        .map_range(
-            &get_bar_region(&pci_info),
-            Attributes::VALID | Attributes::DEVICE_NGNRE | Attributes::EXECUTE_NEVER,
-        )
-        .unwrap();
-
-    info!("Activating IdMap...");
-    trace!("{:?}", idmap);
-    idmap.activate();
-    info!("Activated.");
+    init_page_table(&get_bar_region(&pci_info)).unwrap();
 
     check_data();
     check_dice();
 
+    // SAFETY: This is the only place where `make_pci_root` is called.
     let mut pci_root = unsafe { pci_info.make_pci_root() };
     check_pci(&mut pci_root);
 
@@ -144,42 +109,58 @@
 }
 
 fn check_stack_guard() {
-    const BIONIC_TLS_STACK_GRD_OFF: usize = 40;
-
     info!("Testing stack guard");
-    assert_eq!(bionic_tls(BIONIC_TLS_STACK_GRD_OFF), stack_chk_guard());
+    // SAFETY: No concurrency issue should occur when running these tests.
+    let stack_guard = unsafe { bionic::TLS.stack_guard };
+    assert_ne!(stack_guard, 0);
+    // Check that a NULL-terminating value is added for C functions consuming strings from stack.
+    assert_eq!(stack_guard.to_ne_bytes().last(), Some(&0));
+    // Check that the TLS and guard are properly accessible from the dedicated register.
+    assert_eq!(stack_guard, bionic::__get_tls().stack_guard);
+    // Check that the LLVM __stack_chk_guard alias is also properly set up.
+    assert_eq!(
+        stack_guard,
+        // SAFETY: No concurrency issue should occur when running these tests.
+        unsafe { linker::__stack_chk_guard },
+    );
 }
 
 fn check_data() {
     info!("INITIALISED_DATA: {:?}", INITIALISED_DATA.as_ptr());
-    unsafe {
-        info!("ZEROED_DATA: {:?}", ZEROED_DATA.as_ptr());
-        info!("MUTABLE_DATA: {:?}", MUTABLE_DATA.as_ptr());
-    }
+    // SAFETY: We only print the addresses of the static mutable variable, not actually access it.
+    info!("ZEROED_DATA: {:?}", unsafe { ZEROED_DATA.as_ptr() });
+    // SAFETY: We only print the addresses of the static mutable variable, not actually access it.
+    info!("MUTABLE_DATA: {:?}", unsafe { MUTABLE_DATA.as_ptr() });
 
     assert_eq!(INITIALISED_DATA[0], 1);
     assert_eq!(INITIALISED_DATA[1], 2);
     assert_eq!(INITIALISED_DATA[2], 3);
     assert_eq!(INITIALISED_DATA[3], 4);
 
-    unsafe {
-        for element in ZEROED_DATA.iter() {
-            assert_eq!(*element, 0);
-        }
-        ZEROED_DATA[0] = 13;
-        assert_eq!(ZEROED_DATA[0], 13);
-        ZEROED_DATA[0] = 0;
-        assert_eq!(ZEROED_DATA[0], 0);
+    // SAFETY: Nowhere else in the program accesses this static mutable variable, so there is no
+    // chance of concurrent access.
+    let zeroed_data = unsafe { &mut ZEROED_DATA };
+    // SAFETY: Nowhere else in the program accesses this static mutable variable, so there is no
+    // chance of concurrent access.
+    let mutable_data = unsafe { &mut MUTABLE_DATA };
 
-        assert_eq!(MUTABLE_DATA[0], 1);
-        assert_eq!(MUTABLE_DATA[1], 2);
-        assert_eq!(MUTABLE_DATA[2], 3);
-        assert_eq!(MUTABLE_DATA[3], 4);
-        MUTABLE_DATA[0] += 41;
-        assert_eq!(MUTABLE_DATA[0], 42);
-        MUTABLE_DATA[0] -= 41;
-        assert_eq!(MUTABLE_DATA[0], 1);
+    for element in zeroed_data.iter() {
+        assert_eq!(*element, 0);
     }
+    zeroed_data[0] = 13;
+    assert_eq!(zeroed_data[0], 13);
+    zeroed_data[0] = 0;
+    assert_eq!(zeroed_data[0], 0);
+
+    assert_eq!(mutable_data[0], 1);
+    assert_eq!(mutable_data[1], 2);
+    assert_eq!(mutable_data[2], 3);
+    assert_eq!(mutable_data[3], 4);
+    mutable_data[0] += 41;
+    assert_eq!(mutable_data[0], 42);
+    mutable_data[0] -= 41;
+    assert_eq!(mutable_data[0], 1);
+
     info!("Data looks good");
 }
 
diff --git a/vmbase/example/src/pci.rs b/vmbase/example/src/pci.rs
index 6abe66e..26bc29b 100644
--- a/vmbase/example/src/pci.rs
+++ b/vmbase/example/src/pci.rs
@@ -20,13 +20,14 @@
 use fdtpci::PciInfo;
 use log::{debug, info};
 use virtio_drivers::{
-    device::{blk::VirtIOBlk, console::VirtIOConsole},
+    device::console::VirtIOConsole,
     transport::{
-        pci::{bus::PciRoot, virtio_device_type, PciTransport},
+        pci::{bus::PciRoot, PciTransport},
         DeviceType, Transport,
     },
     BufferDirection, Error, Hal, PhysAddr, PAGE_SIZE,
 };
+use vmbase::virtio::pci::{self, PciTransportIterator};
 
 /// The standard sector size of a VirtIO block device, in bytes.
 const SECTOR_SIZE_BYTES: usize = 512;
@@ -37,39 +38,40 @@
 pub fn check_pci(pci_root: &mut PciRoot) {
     let mut checked_virtio_device_count = 0;
     let mut block_device_count = 0;
-    for (device_function, info) in pci_root.enumerate_bus(0) {
-        let (status, command) = pci_root.get_status_command(device_function);
-        info!("Found {} at {}, status {:?} command {:?}", info, device_function, status, command);
-        if let Some(virtio_type) = virtio_device_type(&info) {
-            info!("  VirtIO {:?}", virtio_type);
-            let mut transport = PciTransport::new::<HalImpl>(pci_root, device_function).unwrap();
-            info!(
-                "Detected virtio PCI device with device type {:?}, features {:#018x}",
-                transport.device_type(),
-                transport.read_device_features(),
-            );
-            match virtio_type {
-                DeviceType::Block => {
-                    check_virtio_block_device(transport, block_device_count);
-                    block_device_count += 1;
-                    checked_virtio_device_count += 1;
-                }
-                DeviceType::Console => {
-                    check_virtio_console_device(transport);
-                    checked_virtio_device_count += 1;
-                }
-                _ => {}
+    let mut socket_device_count = 0;
+    for mut transport in PciTransportIterator::<HalImpl>::new(pci_root) {
+        info!(
+            "Detected virtio PCI device with device type {:?}, features {:#018x}",
+            transport.device_type(),
+            transport.read_device_features(),
+        );
+        match transport.device_type() {
+            DeviceType::Block => {
+                check_virtio_block_device(transport, block_device_count);
+                block_device_count += 1;
+                checked_virtio_device_count += 1;
             }
+            DeviceType::Console => {
+                check_virtio_console_device(transport);
+                checked_virtio_device_count += 1;
+            }
+            DeviceType::Socket => {
+                check_virtio_socket_device(transport);
+                socket_device_count += 1;
+                checked_virtio_device_count += 1;
+            }
+            _ => {}
         }
     }
 
-    assert_eq!(checked_virtio_device_count, 5);
+    assert_eq!(checked_virtio_device_count, 6);
     assert_eq!(block_device_count, 2);
+    assert_eq!(socket_device_count, 1);
 }
 
 /// Checks the given VirtIO block device.
-fn check_virtio_block_device(transport: impl Transport, index: usize) {
-    let mut blk = VirtIOBlk::<HalImpl, _>::new(transport).expect("failed to create blk driver");
+fn check_virtio_block_device(transport: PciTransport, index: usize) {
+    let mut blk = pci::VirtIOBlk::<HalImpl>::new(transport).expect("failed to create blk driver");
     info!("Found {} KiB block device.", blk.capacity() * SECTOR_SIZE_BYTES as u64 / 1024);
     match index {
         0 => {
@@ -93,9 +95,16 @@
     }
 }
 
+/// Checks the given VirtIO socket device.
+fn check_virtio_socket_device(transport: PciTransport) {
+    let socket = pci::VirtIOSocket::<HalImpl>::new(transport)
+        .expect("Failed to create VirtIO socket driver");
+    info!("Found socket device: guest_cid={}", socket.guest_cid());
+}
+
 /// Checks the given VirtIO console device.
-fn check_virtio_console_device(transport: impl Transport) {
-    let mut console = VirtIOConsole::<HalImpl, _>::new(transport)
+fn check_virtio_console_device(transport: PciTransport) {
+    let mut console = VirtIOConsole::<HalImpl, PciTransport>::new(transport)
         .expect("Failed to create VirtIO console driver");
     info!("Found console device: {:?}", console.info());
     for &c in b"Hello VirtIO console\n" {
@@ -111,11 +120,21 @@
 
 struct HalImpl;
 
+/// SAFETY: See the 'Implementation Safety' comments on methods below for how they fulfill the
+/// safety requirements of the unsafe `Hal` trait.
 unsafe impl Hal for HalImpl {
+    /// # Implementation Safety
+    ///
+    /// `dma_alloc` ensures the returned DMA buffer is not aliased with any other allocation or
+    /// reference in the program until it is deallocated by `dma_dealloc` by allocating a unique
+    /// block of memory using `alloc_zeroed`, which is guaranteed to allocate valid, unique and
+    /// zeroed memory. We request an alignment of at least `PAGE_SIZE` from `alloc_zeroed`.
     fn dma_alloc(pages: usize, _direction: BufferDirection) -> (PhysAddr, NonNull<u8>) {
         debug!("dma_alloc: pages={}", pages);
-        let layout = Layout::from_size_align(pages * PAGE_SIZE, PAGE_SIZE).unwrap();
-        // Safe because the layout has a non-zero size.
+        let layout =
+            Layout::from_size_align(pages.checked_mul(PAGE_SIZE).unwrap(), PAGE_SIZE).unwrap();
+        assert_ne!(layout.size(), 0);
+        // SAFETY: We just checked that the layout has a non-zero size.
         let vaddr = unsafe { alloc_zeroed(layout) };
         let vaddr =
             if let Some(vaddr) = NonNull::new(vaddr) { vaddr } else { handle_alloc_error(layout) };
@@ -126,14 +145,19 @@
     unsafe fn dma_dealloc(paddr: PhysAddr, vaddr: NonNull<u8>, pages: usize) -> i32 {
         debug!("dma_dealloc: paddr={:#x}, pages={}", paddr, pages);
         let layout = Layout::from_size_align(pages * PAGE_SIZE, PAGE_SIZE).unwrap();
-        // Safe because the memory was allocated by `dma_alloc` above using the same allocator, and
-        // the layout is the same as was used then.
+        // SAFETY: The memory was allocated by `dma_alloc` above using the same allocator, and the
+        // layout is the same as was used then.
         unsafe {
             dealloc(vaddr.as_ptr(), layout);
         }
         0
     }
 
+    /// # Implementation Safety
+    ///
+    /// The returned pointer must be valid because the `paddr` describes a valid MMIO region, and we
+    /// previously mapped the entire PCI MMIO range. It can't alias any other allocations because
+    /// the PCI MMIO range doesn't overlap with any other memory ranges.
     unsafe fn mmio_phys_to_virt(paddr: PhysAddr, _size: usize) -> NonNull<u8> {
         NonNull::new(paddr as _).unwrap()
     }
diff --git a/vmbase/example/tests/test.rs b/vmbase/example/tests/test.rs
index 085a620..3594523 100644
--- a/vmbase/example/tests/test.rs
+++ b/vmbase/example/tests/test.rs
@@ -106,8 +106,15 @@
     });
     let (handle, console) = android_log_fd()?;
     let (mut log_reader, log_writer) = pipe()?;
-    let vm = VmInstance::create(service.as_ref(), &config, Some(console), Some(log_writer), None)
-        .context("Failed to create VM")?;
+    let vm = VmInstance::create(
+        service.as_ref(),
+        &config,
+        Some(console),
+        /* consoleIn */ None,
+        Some(log_writer),
+        None,
+    )
+    .context("Failed to create VM")?;
     vm.start().context("Failed to start VM")?;
     info!("Started example VM.");
 
diff --git a/vmbase/sections.ld b/vmbase/sections.ld
index 5232d30..c7ef0ec 100644
--- a/vmbase/sections.ld
+++ b/vmbase/sections.ld
@@ -107,6 +107,9 @@
 		. = init_stack_pointer;
 	} >writable_data
 
+	/* Make our Bionic stack protector compatible with mainline LLVM */
+	__stack_chk_guard = __bionic_tls + 40;
+
 	/*
 	 * Remove unused sections from the image.
 	 */
diff --git a/vmbase/src/arch.rs b/vmbase/src/arch.rs
index d7b63b3..d8bb8b2 100644
--- a/vmbase/src/arch.rs
+++ b/vmbase/src/arch.rs
@@ -19,8 +19,8 @@
 macro_rules! read_sysreg {
     ($sysreg:literal) => {{
         let mut r: usize;
-        // Safe because it reads a system register and does not affect Rust.
         #[allow(unused_unsafe)] // In case the macro is used within an unsafe block.
+        // SAFETY: Reading a system register does not affect memory.
         unsafe {
             core::arch::asm!(
                 concat!("mrs {}, ", $sysreg),
@@ -53,8 +53,8 @@
 #[macro_export]
 macro_rules! isb {
     () => {{
-        // Safe because this is just a memory barrier and does not affect Rust.
         #[allow(unused_unsafe)] // In case the macro is used within an unsafe block.
+        // SAFETY: memory barriers do not affect Rust's memory model.
         unsafe {
             core::arch::asm!("isb", options(nomem, nostack, preserves_flags));
         }
@@ -65,8 +65,8 @@
 #[macro_export]
 macro_rules! dsb {
     ($option:literal) => {{
-        // Safe because this is just a memory barrier and does not affect Rust.
         #[allow(unused_unsafe)] // In case the macro is used within an unsafe block.
+        // SAFETY: memory barriers do not affect Rust's memory model.
         unsafe {
             core::arch::asm!(concat!("dsb ", $option), options(nomem, nostack, preserves_flags));
         }
@@ -79,9 +79,9 @@
     ($option:literal, $asid:expr, $addr:expr) => {{
         let asid: usize = $asid;
         let addr: usize = $addr;
-        // Safe because it invalidates TLB and doesn't affect Rust. When the address matches a
-        // block entry larger than the page size, all translations for the block are invalidated.
         #[allow(unused_unsafe)] // In case the macro is used within an unsafe block.
+        // SAFETY: Invalidating the TLB doesn't affect Rust. When the address matches a
+        // block entry larger than the page size, all translations for the block are invalidated.
         unsafe {
             core::arch::asm!(
                 concat!("tlbi ", $option, ", {x}"),
diff --git a/vmbase/src/bionic.rs b/vmbase/src/bionic.rs
index 69da521..29fa0ff 100644
--- a/vmbase/src/bionic.rs
+++ b/vmbase/src/bionic.rs
@@ -22,13 +22,37 @@
 use core::str;
 
 use crate::console;
+use crate::cstr;
 use crate::eprintln;
-use crate::linker;
+use crate::read_sysreg;
 
 const EOF: c_int = -1;
 
-/// Reference to __stack_chk_guard.
-pub static STACK_CHK_GUARD: &u64 = unsafe { &linker::__stack_chk_guard };
+/// Bionic thread-local storage.
+#[repr(C)]
+pub struct Tls {
+    /// Unused.
+    _unused: [u8; 40],
+    /// Use by the compiler as stack canary value.
+    pub stack_guard: u64,
+}
+
+/// Bionic TLS.
+///
+/// Provides the TLS used by Bionic code. This is unique as vmbase only supports one thread.
+///
+/// Note that the linker script re-exports __bionic_tls.stack_guard as __stack_chk_guard for
+/// compatibility with non-Bionic LLVM.
+#[link_section = ".data.stack_protector"]
+#[export_name = "__bionic_tls"]
+pub static mut TLS: Tls = Tls { _unused: [0; 40], stack_guard: 0 };
+
+/// Gets a reference to the TLS from the dedicated system register.
+pub fn __get_tls() -> &'static mut Tls {
+    let tpidr = read_sysreg!("tpidr_el0");
+    // SAFETY: The register is currently only written to once, from entry.S, with a valid value.
+    unsafe { &mut *(tpidr as *mut Tls) }
+}
 
 #[no_mangle]
 extern "C" fn __stack_chk_fail() -> ! {
@@ -46,27 +70,34 @@
 
 #[no_mangle]
 unsafe extern "C" fn __errno() -> *mut c_int {
-    &mut ERRNO as *mut _
+    // SAFETY: C functions which call this are only called from the main thread, not from exception
+    // handlers.
+    unsafe { &mut ERRNO as *mut _ }
 }
 
 fn set_errno(value: c_int) {
-    // SAFETY - vmbase is currently single-threaded.
+    // SAFETY: vmbase is currently single-threaded.
     unsafe { ERRNO = value };
 }
 
+fn get_errno() -> c_int {
+    // SAFETY: vmbase is currently single-threaded.
+    unsafe { ERRNO }
+}
+
 /// Reports a fatal error detected by Bionic.
 ///
 /// # Safety
 ///
-/// Input strings `prefix` and `format` must be properly NULL-terminated.
+/// Input strings `prefix` and `format` must be valid and properly NUL-terminated.
 ///
 /// # Note
 ///
 /// This Rust functions is missing the last argument of its C/C++ counterpart, a va_list.
 #[no_mangle]
 unsafe extern "C" fn async_safe_fatal_va_list(prefix: *const c_char, format: *const c_char) {
-    let prefix = CStr::from_ptr(prefix);
-    let format = CStr::from_ptr(format);
+    // SAFETY: The caller guaranteed that both strings were valid and NUL-terminated.
+    let (prefix, format) = unsafe { (CStr::from_ptr(prefix), CStr::from_ptr(format)) };
 
     if let (Ok(prefix), Ok(format)) = (prefix.to_str(), format.to_str()) {
         // We don't bother with printf formatting.
@@ -100,7 +131,7 @@
 
 #[no_mangle]
 extern "C" fn fputs(c_str: *const c_char, stream: usize) -> c_int {
-    // SAFETY - Just like libc, we need to assume that `s` is a valid NULL-terminated string.
+    // SAFETY: Just like libc, we need to assume that `s` is a valid NULL-terminated string.
     let c_str = unsafe { CStr::from_ptr(c_str) };
 
     if let (Ok(s), Ok(_)) = (c_str.to_str(), File::try_from(stream)) {
@@ -116,7 +147,7 @@
 extern "C" fn fwrite(ptr: *const c_void, size: usize, nmemb: usize, stream: usize) -> usize {
     let length = size.saturating_mul(nmemb);
 
-    // SAFETY - Just like libc, we need to assume that `ptr` is valid.
+    // SAFETY: Just like libc, we need to assume that `ptr` is valid.
     let bytes = unsafe { slice::from_raw_parts(ptr as *const u8, length) };
 
     if let (Ok(s), Ok(_)) = (str::from_utf8(bytes), File::try_from(stream)) {
@@ -129,142 +160,168 @@
 
 #[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",
+    cstr_error(n).as_ptr().cast_mut().cast()
+}
+
+#[no_mangle]
+extern "C" fn perror(s: *const c_char) {
+    let prefix = if s.is_null() {
+        None
+    } else {
+        // SAFETY: Just like libc, we need to assume that `s` is a valid NULL-terminated string.
+        let c_str = unsafe { CStr::from_ptr(s) };
+        // TODO(Rust 1.71): if c_str.is_empty() {
+        if c_str.to_bytes().is_empty() {
+            None
+        } else {
+            Some(c_str.to_str().unwrap())
+        }
     };
 
-    s.as_ptr().cast_mut().cast()
+    let error = cstr_error(get_errno()).to_str().unwrap();
+
+    if let Some(prefix) = prefix {
+        eprintln!("{prefix}: {error}");
+    } else {
+        eprintln!("{error}");
+    }
+}
+
+fn cstr_error(n: c_int) -> &'static CStr {
+    // Messages taken from errno(1).
+    match n {
+        0 => cstr!("Success"),
+        1 => cstr!("Operation not permitted"),
+        2 => cstr!("No such file or directory"),
+        3 => cstr!("No such process"),
+        4 => cstr!("Interrupted system call"),
+        5 => cstr!("Input/output error"),
+        6 => cstr!("No such device or address"),
+        7 => cstr!("Argument list too long"),
+        8 => cstr!("Exec format error"),
+        9 => cstr!("Bad file descriptor"),
+        10 => cstr!("No child processes"),
+        11 => cstr!("Resource temporarily unavailable"),
+        12 => cstr!("Cannot allocate memory"),
+        13 => cstr!("Permission denied"),
+        14 => cstr!("Bad address"),
+        15 => cstr!("Block device required"),
+        16 => cstr!("Device or resource busy"),
+        17 => cstr!("File exists"),
+        18 => cstr!("Invalid cross-device link"),
+        19 => cstr!("No such device"),
+        20 => cstr!("Not a directory"),
+        21 => cstr!("Is a directory"),
+        22 => cstr!("Invalid argument"),
+        23 => cstr!("Too many open files in system"),
+        24 => cstr!("Too many open files"),
+        25 => cstr!("Inappropriate ioctl for device"),
+        26 => cstr!("Text file busy"),
+        27 => cstr!("File too large"),
+        28 => cstr!("No space left on device"),
+        29 => cstr!("Illegal seek"),
+        30 => cstr!("Read-only file system"),
+        31 => cstr!("Too many links"),
+        32 => cstr!("Broken pipe"),
+        33 => cstr!("Numerical argument out of domain"),
+        34 => cstr!("Numerical result out of range"),
+        35 => cstr!("Resource deadlock avoided"),
+        36 => cstr!("File name too long"),
+        37 => cstr!("No locks available"),
+        38 => cstr!("Function not implemented"),
+        39 => cstr!("Directory not empty"),
+        40 => cstr!("Too many levels of symbolic links"),
+        42 => cstr!("No message of desired type"),
+        43 => cstr!("Identifier removed"),
+        44 => cstr!("Channel number out of range"),
+        45 => cstr!("Level 2 not synchronized"),
+        46 => cstr!("Level 3 halted"),
+        47 => cstr!("Level 3 reset"),
+        48 => cstr!("Link number out of range"),
+        49 => cstr!("Protocol driver not attached"),
+        50 => cstr!("No CSI structure available"),
+        51 => cstr!("Level 2 halted"),
+        52 => cstr!("Invalid exchange"),
+        53 => cstr!("Invalid request descriptor"),
+        54 => cstr!("Exchange full"),
+        55 => cstr!("No anode"),
+        56 => cstr!("Invalid request code"),
+        57 => cstr!("Invalid slot"),
+        59 => cstr!("Bad font file format"),
+        60 => cstr!("Device not a stream"),
+        61 => cstr!("No data available"),
+        62 => cstr!("Timer expired"),
+        63 => cstr!("Out of streams resources"),
+        64 => cstr!("Machine is not on the network"),
+        65 => cstr!("Package not installed"),
+        66 => cstr!("Object is remote"),
+        67 => cstr!("Link has been severed"),
+        68 => cstr!("Advertise error"),
+        69 => cstr!("Srmount error"),
+        70 => cstr!("Communication error on send"),
+        71 => cstr!("Protocol error"),
+        72 => cstr!("Multihop attempted"),
+        73 => cstr!("RFS specific error"),
+        74 => cstr!("Bad message"),
+        75 => cstr!("Value too large for defined data type"),
+        76 => cstr!("Name not unique on network"),
+        77 => cstr!("File descriptor in bad state"),
+        78 => cstr!("Remote address changed"),
+        79 => cstr!("Can not access a needed shared library"),
+        80 => cstr!("Accessing a corrupted shared library"),
+        81 => cstr!(".lib section in a.out corrupted"),
+        82 => cstr!("Attempting to link in too many shared libraries"),
+        83 => cstr!("Cannot exec a shared library directly"),
+        84 => cstr!("Invalid or incomplete multibyte or wide character"),
+        85 => cstr!("Interrupted system call should be restarted"),
+        86 => cstr!("Streams pipe error"),
+        87 => cstr!("Too many users"),
+        88 => cstr!("Socket operation on non-socket"),
+        89 => cstr!("Destination address required"),
+        90 => cstr!("Message too long"),
+        91 => cstr!("Protocol wrong type for socket"),
+        92 => cstr!("Protocol not available"),
+        93 => cstr!("Protocol not supported"),
+        94 => cstr!("Socket type not supported"),
+        95 => cstr!("Operation not supported"),
+        96 => cstr!("Protocol family not supported"),
+        97 => cstr!("Address family not supported by protocol"),
+        98 => cstr!("Address already in use"),
+        99 => cstr!("Cannot assign requested address"),
+        100 => cstr!("Network is down"),
+        101 => cstr!("Network is unreachable"),
+        102 => cstr!("Network dropped connection on reset"),
+        103 => cstr!("Software caused connection abort"),
+        104 => cstr!("Connection reset by peer"),
+        105 => cstr!("No buffer space available"),
+        106 => cstr!("Transport endpoint is already connected"),
+        107 => cstr!("Transport endpoint is not connected"),
+        108 => cstr!("Cannot send after transport endpoint shutdown"),
+        109 => cstr!("Too many references: cannot splice"),
+        110 => cstr!("Connection timed out"),
+        111 => cstr!("Connection refused"),
+        112 => cstr!("Host is down"),
+        113 => cstr!("No route to host"),
+        114 => cstr!("Operation already in progress"),
+        115 => cstr!("Operation now in progress"),
+        116 => cstr!("Stale file handle"),
+        117 => cstr!("Structure needs cleaning"),
+        118 => cstr!("Not a XENIX named type file"),
+        119 => cstr!("No XENIX semaphores available"),
+        120 => cstr!("Is a named type file"),
+        121 => cstr!("Remote I/O error"),
+        122 => cstr!("Disk quota exceeded"),
+        123 => cstr!("No medium found"),
+        124 => cstr!("Wrong medium type"),
+        125 => cstr!("Operation canceled"),
+        126 => cstr!("Required key not available"),
+        127 => cstr!("Key has expired"),
+        128 => cstr!("Key has been revoked"),
+        129 => cstr!("Key was rejected by service"),
+        130 => cstr!("Owner died"),
+        131 => cstr!("State not recoverable"),
+        132 => cstr!("Operation not possible due to RF-kill"),
+        133 => cstr!("Memory page has hardware error"),
+        _ => cstr!("Unknown errno value"),
+    }
 }
diff --git a/vmbase/src/console.rs b/vmbase/src/console.rs
index 7c8ddf6..a7d37b4 100644
--- a/vmbase/src/console.rs
+++ b/vmbase/src/console.rs
@@ -25,7 +25,7 @@
 
 /// Initialises a new instance of the UART driver and returns it.
 fn create() -> Uart {
-    // Safe because BASE_ADDRESS is the base of the MMIO region for a UART and is mapped as device
+    // SAFETY: BASE_ADDRESS is the base of the MMIO region for a UART and is mapped as device
     // memory.
     unsafe { Uart::new(BASE_ADDRESS) }
 }
@@ -51,7 +51,7 @@
     write(CONSOLE.lock().as_mut().unwrap(), format_args).unwrap();
 }
 
-/// Reinitialises the UART driver and writes a string to it.
+/// Reinitializes the UART driver and writes a string to it.
 ///
 /// This is intended for use in situations where the UART may be in an unknown state or the global
 /// instance may be locked, such as in an exception handler or panic handler.
@@ -60,7 +60,7 @@
     let _ = uart.write_str(s);
 }
 
-/// Reinitialises the UART driver and writes a formatted string to it.
+/// Reinitializes the UART driver and writes a formatted string to it.
 ///
 /// This is intended for use in situations where the UART may be in an unknown state or the global
 /// instance may be locked, such as in an exception handler or panic handler.
@@ -71,7 +71,7 @@
 
 /// Prints the given formatted string to the console, followed by a newline.
 ///
-/// Panics if the console has not yet been initialised. May hang if used in an exception context;
+/// Panics if the console has not yet been initialized. May hang if used in an exception context;
 /// use `eprintln!` instead.
 macro_rules! println {
     () => ($crate::console::write_str("\n"));
diff --git a/vmbase/src/entry.rs b/vmbase/src/entry.rs
index df0bb7c..2ff66cc 100644
--- a/vmbase/src/entry.rs
+++ b/vmbase/src/entry.rs
@@ -14,14 +14,54 @@
 
 //! Rust entry point.
 
-use crate::{console, heap, power::shutdown};
+use crate::{
+    bionic, console, heap, logger,
+    power::{reboot, shutdown},
+    rand,
+};
+use core::mem::size_of;
+use hyp::{self, get_mmio_guard};
+
+fn try_console_init() -> Result<(), hyp::Error> {
+    console::init();
+
+    if let Some(mmio_guard) = get_mmio_guard() {
+        mmio_guard.enroll()?;
+        mmio_guard.validate_granule()?;
+        mmio_guard.map(console::BASE_ADDRESS)?;
+    }
+
+    Ok(())
+}
 
 /// This is the entry point to the Rust code, called from the binary entry point in `entry.S`.
 #[no_mangle]
 extern "C" fn rust_entry(x0: u64, x1: u64, x2: u64, x3: u64) -> ! {
-    // SAFETY - Only called once, from here, and inaccessible to client code.
+    // SAFETY: Only called once, from here, and inaccessible to client code.
     unsafe { heap::init() };
-    console::init();
+
+    if try_console_init().is_err() {
+        // Don't panic (or log) here to avoid accessing the console.
+        reboot()
+    }
+
+    logger::init().expect("Failed to initialize the logger");
+    // We initialize the logger to Off (like the log crate) and clients should log::set_max_level.
+
+    const SIZE_OF_STACK_GUARD: usize = size_of::<u64>();
+    let mut stack_guard = [0u8; SIZE_OF_STACK_GUARD];
+    // We keep a null byte at the top of the stack guard to act as a string terminator.
+    let random_guard = &mut stack_guard[..(SIZE_OF_STACK_GUARD - 1)];
+
+    rand::init().expect("Failed to initialize a source of entropy");
+    rand::fill_with_entropy(random_guard).expect("Failed to get stack canary entropy");
+    bionic::__get_tls().stack_guard = u64::from_ne_bytes(stack_guard);
+
+    // Note: If rust_entry ever returned (which it shouldn't by being -> !), the compiler-injected
+    // stack guard comparison would detect a mismatch and call __stack_chk_fail.
+
+    // SAFETY: `main` is provided by the application using the `main!` macro, and we make sure it
+    // has the right type.
     unsafe {
         main(x0, x1, x2, x3);
     }
@@ -35,16 +75,21 @@
 
 /// Marks the main function of the binary.
 ///
+/// Once main is entered, it can assume that:
+/// - The panic_handler has been configured and panic!() and friends are available;
+/// - The global_allocator has been configured and heap memory is available;
+/// - The logger has been configured and the log::{info, warn, error, ...} macros are available.
+///
 /// Example:
 ///
 /// ```rust
-/// use vmbase::{logger, main};
+/// use vmbase::main;
 /// use log::{info, LevelFilter};
 ///
 /// main!(my_main);
 ///
 /// fn my_main() {
-///     logger::init(LevelFilter::Info).unwrap();
+///     log::set_max_level(LevelFilter::Info);
 ///     info!("Hello world");
 /// }
 /// ```
diff --git a/vmbase/src/exceptions.rs b/vmbase/src/exceptions.rs
new file mode 100644
index 0000000..7833334
--- /dev/null
+++ b/vmbase/src/exceptions.rs
@@ -0,0 +1,139 @@
+// 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.
+
+//! Helper functions and structs for exception handlers.
+
+use crate::{
+    console, eprintln,
+    memory::{page_4kb_of, MemoryTrackerError},
+    read_sysreg,
+};
+use aarch64_paging::paging::VirtualAddress;
+use core::fmt;
+
+const UART_PAGE: usize = page_4kb_of(console::BASE_ADDRESS);
+
+/// Represents an error that can occur while handling an exception.
+#[derive(Debug)]
+pub enum HandleExceptionError {
+    /// The page table is unavailable.
+    PageTableUnavailable,
+    /// The page table has not been initialized.
+    PageTableNotInitialized,
+    /// An internal error occurred in the memory tracker.
+    InternalError(MemoryTrackerError),
+    /// An unknown exception occurred.
+    UnknownException,
+}
+
+impl From<MemoryTrackerError> for HandleExceptionError {
+    fn from(other: MemoryTrackerError) -> Self {
+        Self::InternalError(other)
+    }
+}
+
+impl fmt::Display for HandleExceptionError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Self::PageTableUnavailable => write!(f, "Page table is not available."),
+            Self::PageTableNotInitialized => write!(f, "Page table is not initialized."),
+            Self::InternalError(e) => write!(f, "Error while updating page table: {e}"),
+            Self::UnknownException => write!(f, "An unknown exception occurred, not handled."),
+        }
+    }
+}
+
+/// Represents the possible types of exception syndrome register (ESR) values.
+#[derive(Debug, PartialEq, Copy, Clone)]
+pub enum Esr {
+    /// Data abort due to translation fault.
+    DataAbortTranslationFault,
+    /// Data abort due to permission fault.
+    DataAbortPermissionFault,
+    /// Data abort due to a synchronous external abort.
+    DataAbortSyncExternalAbort,
+    /// An unknown ESR value.
+    Unknown(usize),
+}
+
+impl Esr {
+    const EXT_DABT_32BIT: usize = 0x96000010;
+    const TRANSL_FAULT_BASE_32BIT: usize = 0x96000004;
+    const TRANSL_FAULT_ISS_MASK_32BIT: usize = !0x143;
+    const PERM_FAULT_BASE_32BIT: usize = 0x9600004C;
+    const PERM_FAULT_ISS_MASK_32BIT: usize = !0x103;
+}
+
+impl From<usize> for Esr {
+    fn from(esr: usize) -> Self {
+        if esr == Self::EXT_DABT_32BIT {
+            Self::DataAbortSyncExternalAbort
+        } else if esr & Self::TRANSL_FAULT_ISS_MASK_32BIT == Self::TRANSL_FAULT_BASE_32BIT {
+            Self::DataAbortTranslationFault
+        } else if esr & Self::PERM_FAULT_ISS_MASK_32BIT == Self::PERM_FAULT_BASE_32BIT {
+            Self::DataAbortPermissionFault
+        } else {
+            Self::Unknown(esr)
+        }
+    }
+}
+
+impl fmt::Display for Esr {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Self::DataAbortSyncExternalAbort => write!(f, "Synchronous external abort"),
+            Self::DataAbortTranslationFault => write!(f, "Translation fault"),
+            Self::DataAbortPermissionFault => write!(f, "Permission fault"),
+            Self::Unknown(v) => write!(f, "Unknown exception esr={v:#08x}"),
+        }
+    }
+}
+/// A struct representing an Armv8 exception.
+pub struct ArmException {
+    /// The value of the exception syndrome register.
+    pub esr: Esr,
+    /// The faulting virtual address read from the fault address register.
+    pub far: VirtualAddress,
+}
+
+impl fmt::Display for ArmException {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "ArmException: esr={}, far={}", self.esr, self.far)
+    }
+}
+
+impl ArmException {
+    /// Reads the values of the EL1 exception syndrome register (`esr_el1`)
+    /// and fault address register (`far_el1`) and returns a new instance of
+    /// `ArmException` with these values.
+    pub fn from_el1_regs() -> Self {
+        let esr: Esr = read_sysreg!("esr_el1").into();
+        let far = read_sysreg!("far_el1");
+        Self { esr, far: VirtualAddress(far) }
+    }
+
+    /// Prints the details of an obj and the exception, excluding UART exceptions.
+    pub fn print<T: fmt::Display>(&self, exception_name: &str, obj: T, elr: u64) {
+        // Don't print to the UART if we are handling an exception it could raise.
+        if !self.is_uart_exception() {
+            eprintln!("{exception_name}");
+            eprintln!("{obj}");
+            eprintln!("{}, elr={:#08x}", self, elr);
+        }
+    }
+
+    fn is_uart_exception(&self) -> bool {
+        self.esr == Esr::DataAbortSyncExternalAbort && page_4kb_of(self.far.0) == UART_PAGE
+    }
+}
diff --git a/vmbase/src/heap.rs b/vmbase/src/heap.rs
index b00ca6f..c8b76ac 100644
--- a/vmbase/src/heap.rs
+++ b/vmbase/src/heap.rs
@@ -33,7 +33,7 @@
     ($len:expr) => {
         static mut __HEAP_ARRAY: [u8; $len] = [0; $len];
         #[export_name = "HEAP"]
-        // SAFETY - HEAP will only be accessed once as mut, from init().
+        // SAFETY: HEAP will only be accessed once as mut, from init().
         static mut __HEAP: &'static mut [u8] = unsafe { &mut __HEAP_ARRAY };
     };
 }
@@ -65,12 +65,12 @@
 pub fn aligned_boxed_slice(size: usize, align: usize) -> Option<Box<[u8]>> {
     let size = NonZeroUsize::new(size)?.get();
     let layout = Layout::from_size_align(size, align).ok()?;
-    // SAFETY - We verify that `size` and the returned `ptr` are non-null.
+    // SAFETY: We verify that `size` and the returned `ptr` are non-null.
     let ptr = unsafe { alloc(layout) };
     let ptr = NonNull::new(ptr)?.as_ptr();
     let slice_ptr = ptr::slice_from_raw_parts_mut(ptr, size);
 
-    // SAFETY - The memory was allocated using the proper layout by our global_allocator.
+    // SAFETY: The memory was allocated using the proper layout by our global_allocator.
     Some(unsafe { Box::from_raw(slice_ptr) })
 }
 
@@ -100,9 +100,9 @@
         heap_range.contains(&(ptr.as_ptr() as *const u8)),
         "free() called on a pointer that is not part of the HEAP: {ptr:?}"
     );
+    // SAFETY: ptr is non-null and was allocated by allocate, which prepends a correctly aligned
+    // usize.
     let (ptr, size) = unsafe {
-        // SAFETY: ptr is non-null and was allocated by allocate, which prepends a correctly aligned
-        // usize.
         let ptr = ptr.cast::<usize>().as_ptr().offset(-1);
         (ptr, *ptr)
     };
diff --git a/vmbase/src/hvc.rs b/vmbase/src/hvc.rs
index 9a5e716..1197143 100644
--- a/vmbase/src/hvc.rs
+++ b/vmbase/src/hvc.rs
@@ -22,23 +22,22 @@
 };
 
 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;
+pub const ARM_SMCCC_TRNG_RND64: u32 = 0xc400_0053;
 
 /// Returns the (major, minor) version tuple, as defined by the SMCCC TRNG.
-pub fn trng_version() -> trng::Result<(u16, u16)> {
+pub fn trng_version() -> trng::Result<trng::Version> {
     let args = [0u64; 17];
 
     let version = positive_or_error_64::<Error>(hvc64(ARM_SMCCC_TRNG_VERSION, args)[0])?;
-    Ok(((version >> 16) as u16, version as u16))
+    (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];
@@ -47,5 +46,12 @@
     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> {
+    let mut args = [0u64; 17];
+    args[0] = fid as u64;
+
+    positive_or_error_64::<Error>(hvc64(ARM_SMCCC_TRNG_FEATURES, args)[0])
 }
diff --git a/vmbase/src/hvc/trng.rs b/vmbase/src/hvc/trng.rs
index 6331d66..efb86f6 100644
--- a/vmbase/src/hvc/trng.rs
+++ b/vmbase/src/hvc/trng.rs
@@ -16,7 +16,7 @@
 use core::result;
 
 /// Standard SMCCC TRNG error values as described in DEN 0098 1.0 REL0.
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq)]
 pub enum Error {
     /// The call is not supported by the implementation.
     NotSupported,
@@ -55,3 +55,40 @@
 }
 
 pub type Result<T> = result::Result<T, Error>;
+
+/// A version of the SMCCC TRNG interface.
+#[derive(Copy, Clone, Eq, Ord, PartialEq, PartialOrd)]
+pub struct Version {
+    pub major: u16,
+    pub minor: u16,
+}
+
+impl fmt::Display for Version {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "{}.{}", self.major, self.minor)
+    }
+}
+
+impl fmt::Debug for Version {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        fmt::Display::fmt(self, f)
+    }
+}
+
+impl TryFrom<i32> for Version {
+    type Error = Error;
+
+    fn try_from(value: i32) -> core::result::Result<Self, Error> {
+        if value < 0 {
+            Err((value as i64).into())
+        } else {
+            Ok(Self { major: (value >> 16) as u16, minor: value as u16 })
+        }
+    }
+}
+
+impl From<Version> for u32 {
+    fn from(version: Version) -> Self {
+        (u32::from(version.major) << 16) | u32::from(version.minor)
+    }
+}
diff --git a/vmbase/src/layout/mod.rs b/vmbase/src/layout/mod.rs
index 21c113a..f7e8170 100644
--- a/vmbase/src/layout/mod.rs
+++ b/vmbase/src/layout/mod.rs
@@ -17,6 +17,8 @@
 pub mod crosvm;
 
 use crate::console::BASE_ADDRESS;
+use crate::linker::__stack_chk_guard;
+use aarch64_paging::paging::VirtualAddress;
 use core::ops::Range;
 use core::ptr::addr_of;
 
@@ -27,11 +29,14 @@
 #[macro_export]
 macro_rules! linker_addr {
     ($symbol:ident) => {{
-        unsafe { addr_of!($crate::linker::$symbol) as usize }
+        // SAFETY: We're just getting the address of an extern static symbol provided by the linker,
+        // not dereferencing it.
+        let addr = unsafe { addr_of!($crate::linker::$symbol) as usize };
+        VirtualAddress(addr)
     }};
 }
 
-/// Get the address range between a pair of linker-defined symbols.
+/// Gets the virtual address range between a pair of linker-defined symbols.
 #[macro_export]
 macro_rules! linker_region {
     ($begin:ident,$end:ident) => {{
@@ -43,57 +48,65 @@
 }
 
 /// Memory reserved for the DTB.
-pub fn dtb_range() -> Range<usize> {
+pub fn dtb_range() -> Range<VirtualAddress> {
     linker_region!(dtb_begin, dtb_end)
 }
 
 /// Executable code.
-pub fn text_range() -> Range<usize> {
+pub fn text_range() -> Range<VirtualAddress> {
     linker_region!(text_begin, text_end)
 }
 
 /// Read-only data.
-pub fn rodata_range() -> Range<usize> {
+pub fn rodata_range() -> Range<VirtualAddress> {
     linker_region!(rodata_begin, rodata_end)
 }
 
 /// Initialised writable data.
-pub fn data_range() -> Range<usize> {
+pub fn data_range() -> Range<VirtualAddress> {
     linker_region!(data_begin, data_end)
 }
 
-/// Zero-initialised writable data.
-pub fn bss_range() -> Range<usize> {
+/// Zero-initialized writable data.
+pub fn bss_range() -> Range<VirtualAddress> {
     linker_region!(bss_begin, bss_end)
 }
 
 /// Writable data region for the stack.
-pub fn stack_range(stack_size: usize) -> Range<usize> {
+pub fn stack_range(stack_size: usize) -> Range<VirtualAddress> {
     let end = linker_addr!(init_stack_pointer);
-    let start = end.checked_sub(stack_size).unwrap();
+    let start = VirtualAddress(end.0.checked_sub(stack_size).unwrap());
     assert!(start >= linker_addr!(stack_limit));
 
     start..end
 }
 
 /// All writable sections, excluding the stack.
-pub fn scratch_range() -> Range<usize> {
+pub fn scratch_range() -> Range<VirtualAddress> {
     linker_region!(eh_stack_limit, bss_end)
 }
 
 /// UART console range.
-pub fn console_uart_range() -> Range<usize> {
+pub fn console_uart_range() -> Range<VirtualAddress> {
     const CONSOLE_LEN: usize = 1; // `uart::Uart` only uses one u8 register.
 
-    BASE_ADDRESS..(BASE_ADDRESS + CONSOLE_LEN)
+    VirtualAddress(BASE_ADDRESS)..VirtualAddress(BASE_ADDRESS + CONSOLE_LEN)
 }
 
 /// Read-write data (original).
-pub fn data_load_address() -> usize {
+pub fn data_load_address() -> VirtualAddress {
     linker_addr!(data_lma)
 }
 
 /// End of the binary image.
-pub fn binary_end() -> usize {
+pub fn binary_end() -> VirtualAddress {
     linker_addr!(bin_end)
 }
+
+/// Value of __stack_chk_guard.
+pub fn stack_chk_guard() -> u64 {
+    // SAFETY: __stack_chk_guard shouldn't have any mutable aliases unless the stack overflows. If
+    // it does, then there could be undefined behaviour all over the program, but we want to at
+    // least have a chance at catching it.
+    unsafe { addr_of!(__stack_chk_guard).read_volatile() }
+}
diff --git a/vmbase/src/lib.rs b/vmbase/src/lib.rs
index 88bad8b..ca8756d 100644
--- a/vmbase/src/lib.rs
+++ b/vmbase/src/lib.rs
@@ -15,18 +15,21 @@
 //! Basic functionality for bare-metal binaries to run in a VM under crosvm.
 
 #![no_std]
+#![deny(unsafe_op_in_unsafe_fn)]
+#![deny(clippy::undocumented_unsafe_blocks)]
 
 extern crate alloc;
 
 pub mod arch;
-mod bionic;
+pub mod bionic;
 pub mod console;
 mod entry;
+pub mod exceptions;
 pub mod fdt;
 pub mod heap;
 mod hvc;
 pub mod layout;
-mod linker;
+pub mod linker;
 pub mod logger;
 pub mod memory;
 pub mod power;
@@ -35,8 +38,6 @@
 pub mod util;
 pub mod virtio;
 
-pub use bionic::STACK_CHK_GUARD;
-
 use core::panic::PanicInfo;
 use power::reboot;
 
diff --git a/vmbase/src/logger.rs b/vmbase/src/logger.rs
index c30adad..9130918 100644
--- a/vmbase/src/logger.rs
+++ b/vmbase/src/logger.rs
@@ -20,19 +20,20 @@
 
 use crate::console::println;
 use core::sync::atomic::{AtomicBool, Ordering};
-use log::{LevelFilter, Log, Metadata, Record, SetLoggerError};
+use log::{Log, Metadata, Record, SetLoggerError};
 
 struct Logger {
     is_enabled: AtomicBool,
 }
-static mut LOGGER: Logger = Logger::new();
+
+static LOGGER: Logger = Logger::new();
 
 impl Logger {
     const fn new() -> Self {
         Self { is_enabled: AtomicBool::new(true) }
     }
 
-    fn swap_enabled(&mut self, enabled: bool) -> bool {
+    fn swap_enabled(&self, enabled: bool) -> bool {
         self.is_enabled.swap(enabled, Ordering::Relaxed)
     }
 }
@@ -58,27 +59,19 @@
 
 impl SuppressGuard {
     fn new() -> Self {
-        // Safe because it modifies an atomic.
-        unsafe { Self { old_enabled: LOGGER.swap_enabled(false) } }
+        Self { old_enabled: LOGGER.swap_enabled(false) }
     }
 }
 
 impl Drop for SuppressGuard {
     fn drop(&mut self) {
-        // Safe because it modifies an atomic.
-        unsafe {
-            LOGGER.swap_enabled(self.old_enabled);
-        }
+        LOGGER.swap_enabled(self.old_enabled);
     }
 }
 
 /// Initialize vmbase logger with a given max logging level.
-pub fn init(max_level: LevelFilter) -> Result<(), SetLoggerError> {
-    // Safe because it only sets the global logger.
-    unsafe {
-        log::set_logger(&LOGGER)?;
-    }
-    log::set_max_level(max_level);
+pub(crate) fn init() -> Result<(), SetLoggerError> {
+    log::set_logger(&LOGGER)?;
     Ok(())
 }
 
diff --git a/vmbase/src/memory/dbm.rs b/vmbase/src/memory/dbm.rs
index d429b30..401022e 100644
--- a/vmbase/src/memory/dbm.rs
+++ b/vmbase/src/memory/dbm.rs
@@ -34,7 +34,7 @@
     } else {
         tcr &= !TCR_EL1_HA_HD_BITS
     };
-    // Safe because it writes to a system register and does not affect Rust.
+    // SAFETY: Changing this bit in TCR doesn't affect Rust's view of memory.
     unsafe { write_sysreg!("tcr_el1", tcr) }
     isb!();
 }
diff --git a/vmbase/src/memory/mod.rs b/vmbase/src/memory/mod.rs
index 5e78565..2f72fc4 100644
--- a/vmbase/src/memory/mod.rs
+++ b/vmbase/src/memory/mod.rs
@@ -22,8 +22,13 @@
 
 pub use error::MemoryTrackerError;
 pub use page_table::PageTable;
-pub use shared::{alloc_shared, dealloc_shared, MemoryRange, MemoryTracker, MEMORY};
-pub use util::{
-    flush, flushed_zeroize, min_dcache_line_size, page_4kb_of, phys_to_virt, virt_to_phys,
-    PAGE_SIZE, SIZE_128KB, SIZE_2MB, SIZE_4KB, SIZE_4MB, SIZE_64KB,
+pub use shared::{
+    handle_permission_fault, handle_translation_fault, MemoryRange, MemoryTracker, MEMORY,
 };
+pub use util::{
+    flush, flushed_zeroize, min_dcache_line_size, page_4kb_of, PAGE_SIZE, SIZE_128KB, SIZE_2MB,
+    SIZE_4KB, SIZE_4MB, SIZE_64KB,
+};
+
+pub(crate) use shared::{alloc_shared, dealloc_shared};
+pub(crate) use util::{phys_to_virt, virt_to_phys};
diff --git a/vmbase/src/memory/page_table.rs b/vmbase/src/memory/page_table.rs
index 3943b03..e067e96 100644
--- a/vmbase/src/memory/page_table.rs
+++ b/vmbase/src/memory/page_table.rs
@@ -18,7 +18,7 @@
 use aarch64_paging::idmap::IdMap;
 use aarch64_paging::paging::{Attributes, MemoryRegion, PteUpdater};
 use aarch64_paging::MapError;
-use core::{ops::Range, result};
+use core::result;
 
 /// Software bit used to indicate a device that should be lazily mapped.
 pub(super) const MMIO_LAZY_MAP_FLAG: Attributes = Attributes::SWFLAG_0;
@@ -88,50 +88,44 @@
 
     /// Maps the given range of virtual addresses to the physical addresses as lazily mapped
     /// nGnRE device memory.
-    pub fn map_device_lazy(&mut self, range: &Range<usize>) -> Result<()> {
-        self.map_range(range, DEVICE_LAZY)
+    pub fn map_device_lazy(&mut self, range: &MemoryRegion) -> Result<()> {
+        self.idmap.map_range(range, DEVICE_LAZY)
     }
 
     /// Maps the given range of virtual addresses to the physical addresses as valid device
     /// nGnRE device memory.
-    pub fn map_device(&mut self, range: &Range<usize>) -> Result<()> {
-        self.map_range(range, DEVICE)
+    pub fn map_device(&mut self, range: &MemoryRegion) -> Result<()> {
+        self.idmap.map_range(range, DEVICE)
     }
 
     /// Maps the given range of virtual addresses to the physical addresses as non-executable
     /// and writable normal memory.
-    pub fn map_data(&mut self, range: &Range<usize>) -> Result<()> {
-        self.map_range(range, DATA)
+    pub fn map_data(&mut self, range: &MemoryRegion) -> Result<()> {
+        self.idmap.map_range(range, DATA)
     }
 
     /// Maps the given range of virtual addresses to the physical addresses as non-executable,
     /// read-only and writable-clean normal memory.
-    pub fn map_data_dbm(&mut self, range: &Range<usize>) -> Result<()> {
-        self.map_range(range, DATA_DBM)
+    pub fn map_data_dbm(&mut self, range: &MemoryRegion) -> Result<()> {
+        self.idmap.map_range(range, DATA_DBM)
     }
 
     /// Maps the given range of virtual addresses to the physical addresses as read-only
     /// normal memory.
-    pub fn map_code(&mut self, range: &Range<usize>) -> Result<()> {
-        self.map_range(range, CODE)
+    pub fn map_code(&mut self, range: &MemoryRegion) -> Result<()> {
+        self.idmap.map_range(range, CODE)
     }
 
     /// Maps the given range of virtual addresses to the physical addresses as non-executable
     /// and read-only normal memory.
-    pub fn map_rodata(&mut self, range: &Range<usize>) -> Result<()> {
-        self.map_range(range, RODATA)
-    }
-
-    /// Maps the given range of virtual addresses to the physical addresses with the given
-    /// attributes.
-    fn map_range(&mut self, range: &Range<usize>, attr: Attributes) -> Result<()> {
-        self.idmap.map_range(&MemoryRegion::new(range.start, range.end), attr)
+    pub fn map_rodata(&mut self, range: &MemoryRegion) -> Result<()> {
+        self.idmap.map_range(range, RODATA)
     }
 
     /// Applies the provided updater function to a number of PTEs corresponding to a given memory
     /// range.
-    pub fn modify_range(&mut self, range: &Range<usize>, f: &PteUpdater) -> Result<()> {
-        self.idmap.modify_range(&MemoryRegion::new(range.start, range.end), f)
+    pub fn modify_range(&mut self, range: &MemoryRegion, f: &PteUpdater) -> Result<()> {
+        self.idmap.modify_range(range, f)
     }
 }
 
diff --git a/vmbase/src/memory/shared.rs b/vmbase/src/memory/shared.rs
index 61cbeb0..064fb6d 100644
--- a/vmbase/src/memory/shared.rs
+++ b/vmbase/src/memory/shared.rs
@@ -19,18 +19,21 @@
 use super::page_table::{is_leaf_pte, PageTable, MMIO_LAZY_MAP_FLAG};
 use super::util::{page_4kb_of, virt_to_phys};
 use crate::dsb;
+use crate::exceptions::HandleExceptionError;
 use crate::util::RangeExt as _;
-use aarch64_paging::paging::{Attributes, Descriptor, MemoryRegion as VaRange};
+use aarch64_paging::paging::{Attributes, Descriptor, MemoryRegion as VaRange, VirtualAddress};
 use alloc::alloc::{alloc_zeroed, dealloc, handle_alloc_error};
 use alloc::boxed::Box;
 use alloc::vec::Vec;
 use buddy_system_allocator::{FrameAllocator, LockedFrameAllocator};
 use core::alloc::Layout;
+use core::cmp::max;
+use core::mem::size_of;
 use core::num::NonZeroUsize;
 use core::ops::Range;
 use core::ptr::NonNull;
 use core::result;
-use hyp::{get_hypervisor, MMIO_GUARD_GRANULE_SIZE};
+use hyp::{get_mem_sharer, get_mmio_guard, MMIO_GUARD_GRANULE_SIZE};
 use log::{debug, error, trace};
 use once_cell::race::OnceBox;
 use spin::mutex::SpinMutex;
@@ -44,6 +47,11 @@
 
 /// Memory range.
 pub type MemoryRange = Range<usize>;
+
+fn get_va_range(range: &MemoryRange) -> VaRange {
+    VaRange::new(range.start, range.end)
+}
+
 type Result<T> = result::Result<T, MemoryTrackerError>;
 
 #[derive(Clone, Copy, Debug, Default, PartialEq)]
@@ -69,6 +77,8 @@
     payload_range: Option<MemoryRange>,
 }
 
+// TODO: Remove this once aarch64-paging crate is updated.
+// SAFETY: Only `PageTable` doesn't implement Send, but it should.
 unsafe impl Send for MemoryTracker {}
 
 impl MemoryTracker {
@@ -80,7 +90,7 @@
         mut page_table: PageTable,
         total: MemoryRange,
         mmio_range: MemoryRange,
-        payload_range: Option<MemoryRange>,
+        payload_range: Option<Range<VirtualAddress>>,
     ) -> Self {
         assert!(
             !total.overlaps(&mmio_range),
@@ -93,7 +103,7 @@
         set_dbm_enabled(true);
 
         debug!("Activating dynamic page table...");
-        // SAFETY - page_table duplicates the static mappings for everything that the Rust code is
+        // SAFETY: page_table duplicates the static mappings for everything that the Rust code is
         // aware of so activating it shouldn't have any visible effect.
         unsafe { page_table.activate() }
         debug!("... Success!");
@@ -104,7 +114,7 @@
             regions: ArrayVec::new(),
             mmio_regions: ArrayVec::new(),
             mmio_range,
-            payload_range,
+            payload_range: payload_range.map(|r| r.start.0..r.end.0),
         }
     }
 
@@ -130,7 +140,7 @@
     pub fn alloc_range(&mut self, range: &MemoryRange) -> Result<MemoryRange> {
         let region = MemoryRegion { range: range.clone(), mem_type: MemoryType::ReadOnly };
         self.check(&region)?;
-        self.page_table.map_rodata(range).map_err(|e| {
+        self.page_table.map_rodata(&get_va_range(range)).map_err(|e| {
             error!("Error during range allocation: {e}");
             MemoryTrackerError::FailedToMap
         })?;
@@ -141,7 +151,7 @@
     pub fn alloc_range_mut(&mut self, range: &MemoryRange) -> Result<MemoryRange> {
         let region = MemoryRegion { range: range.clone(), mem_type: MemoryType::ReadWrite };
         self.check(&region)?;
-        self.page_table.map_data_dbm(range).map_err(|e| {
+        self.page_table.map_data_dbm(&get_va_range(range)).map_err(|e| {
             error!("Error during mutable range allocation: {e}");
             MemoryTrackerError::FailedToMap
         })?;
@@ -171,10 +181,17 @@
             return Err(MemoryTrackerError::Full);
         }
 
-        self.page_table.map_device_lazy(&range).map_err(|e| {
-            error!("Error during MMIO device mapping: {e}");
-            MemoryTrackerError::FailedToMap
-        })?;
+        if get_mmio_guard().is_some() {
+            self.page_table.map_device_lazy(&get_va_range(&range)).map_err(|e| {
+                error!("Error during lazy MMIO device mapping: {e}");
+                MemoryTrackerError::FailedToMap
+            })?;
+        } else {
+            self.page_table.map_device(&get_va_range(&range)).map_err(|e| {
+                error!("Error during MMIO device mapping: {e}");
+                MemoryTrackerError::FailedToMap
+            })?;
+        }
 
         if self.mmio_regions.try_push(range).is_some() {
             return Err(MemoryTrackerError::Full);
@@ -211,10 +228,12 @@
     ///
     /// Note that they are not unmapped from the page table.
     pub fn mmio_unmap_all(&mut self) -> Result<()> {
-        for range in &self.mmio_regions {
-            self.page_table
-                .modify_range(range, &mmio_guard_unmap_page)
-                .map_err(|_| MemoryTrackerError::FailedToUnmap)?;
+        if get_mmio_guard().is_some() {
+            for range in &self.mmio_regions {
+                self.page_table
+                    .modify_range(&get_va_range(range), &mmio_guard_unmap_page)
+                    .map_err(|_| MemoryTrackerError::FailedToUnmap)?;
+            }
         }
         Ok(())
     }
@@ -256,6 +275,19 @@
         Ok(())
     }
 
+    /// Initialize the shared heap to use heap memory directly.
+    ///
+    /// When running on "non-protected" hypervisors which permit host direct accesses to guest
+    /// memory, there is no need to perform any memory sharing and/or allocate buffers from a
+    /// dedicated region so this function instructs the shared pool to use the global allocator.
+    pub fn init_heap_shared_pool(&mut self) -> Result<()> {
+        // As MemorySharer only calls MEM_SHARE methods if the hypervisor supports them, internally
+        // using init_dynamic_shared_pool() on a non-protected platform will make use of the heap
+        // without any actual "dynamic memory sharing" taking place and, as such, the granule may
+        // be set to the one of the global_allocator i.e. a byte.
+        self.init_dynamic_shared_pool(size_of::<u8>())
+    }
+
     /// Unshares any memory that may have been shared.
     pub fn unshare_all_memory(&mut self) {
         drop(SHARED_MEMORY.lock().take());
@@ -263,12 +295,14 @@
 
     /// Handles translation fault for blocks flagged for lazy MMIO mapping by enabling the page
     /// table entry and MMIO guard mapping the block. Breaks apart a block entry if required.
-    pub fn handle_mmio_fault(&mut self, addr: usize) -> Result<()> {
-        let page_range = page_4kb_of(addr)..page_4kb_of(addr) + MMIO_GUARD_GRANULE_SIZE;
+    fn handle_mmio_fault(&mut self, addr: VirtualAddress) -> Result<()> {
+        let page_start = VirtualAddress(page_4kb_of(addr.0));
+        let page_range: VaRange = (page_start..page_start + MMIO_GUARD_GRANULE_SIZE).into();
+        let mmio_guard = get_mmio_guard().unwrap();
         self.page_table
             .modify_range(&page_range, &verify_lazy_mapped_block)
             .map_err(|_| MemoryTrackerError::InvalidPte)?;
-        get_hypervisor().mmio_guard_map(page_range.start)?;
+        mmio_guard.map(page_start.0)?;
         // Maps a single device page, breaking up block mappings if necessary.
         self.page_table.map_device(&page_range).map_err(|_| MemoryTrackerError::FailedToMap)
     }
@@ -284,7 +318,7 @@
         // Now flush writable-dirty pages in those regions.
         for range in writable_regions.chain(self.payload_range.as_ref().into_iter()) {
             self.page_table
-                .modify_range(range, &flush_dirty_range)
+                .modify_range(&get_va_range(range), &flush_dirty_range)
                 .map_err(|_| MemoryTrackerError::FlushRegionFailed)?;
         }
         Ok(())
@@ -293,9 +327,9 @@
     /// Handles permission fault for read-only blocks by setting writable-dirty state.
     /// In general, this should be called from the exception handler when hardware dirty
     /// state management is disabled or unavailable.
-    pub fn handle_permission_fault(&mut self, addr: usize) -> Result<()> {
+    fn handle_permission_fault(&mut self, addr: VirtualAddress) -> Result<()> {
         self.page_table
-            .modify_range(&(addr..addr + 1), &mark_dirty_block)
+            .modify_range(&(addr..addr + 1).into(), &mark_dirty_block)
             .map_err(|_| MemoryTrackerError::SetPteDirtyFailed)
     }
 }
@@ -310,7 +344,7 @@
 
 /// Allocates a memory range of at least the given size and alignment that is shared with the host.
 /// Returns a pointer to the buffer.
-pub fn alloc_shared(layout: Layout) -> hyp::Result<NonNull<u8>> {
+pub(crate) fn alloc_shared(layout: Layout) -> hyp::Result<NonNull<u8>> {
     assert_ne!(layout.size(), 0);
     let Some(buffer) = try_shared_alloc(layout) else {
         handle_alloc_error(layout);
@@ -326,7 +360,11 @@
     if let Some(buffer) = shared_pool.alloc_aligned(layout) {
         Some(NonNull::new(buffer as _).unwrap())
     } else if let Some(shared_memory) = SHARED_MEMORY.lock().as_mut() {
-        shared_memory.refill(&mut shared_pool, layout);
+        // Adjusts the layout size to the max of the next power of two and the alignment,
+        // as this is the actual size of the memory allocated in `alloc_aligned()`.
+        let size = max(layout.size().next_power_of_two(), layout.align());
+        let refill_layout = Layout::from_size_align(size, layout.align()).unwrap();
+        shared_memory.refill(&mut shared_pool, refill_layout);
         shared_pool.alloc_aligned(layout).map(|buffer| NonNull::new(buffer as _).unwrap())
     } else {
         None
@@ -341,7 +379,7 @@
 ///
 /// The memory must have been allocated by `alloc_shared` with the same layout, and not yet
 /// deallocated.
-pub unsafe fn dealloc_shared(vaddr: NonNull<u8>, layout: Layout) -> hyp::Result<()> {
+pub(crate) unsafe fn dealloc_shared(vaddr: NonNull<u8>, layout: Layout) -> hyp::Result<()> {
     SHARED_POOL.get().unwrap().lock().dealloc_aligned(vaddr.as_ptr() as usize, layout);
 
     trace!("Deallocated shared buffer at {vaddr:?} with {layout:?}");
@@ -353,7 +391,7 @@
 /// Unshares all pages when dropped.
 struct MemorySharer {
     granule: usize,
-    shared_regions: Vec<(usize, Layout)>,
+    frames: Vec<(usize, Layout)>,
 }
 
 impl MemorySharer {
@@ -361,42 +399,47 @@
     /// `granule` must be a power of 2.
     fn new(granule: usize, capacity: usize) -> Self {
         assert!(granule.is_power_of_two());
-        Self { granule, shared_regions: Vec::with_capacity(capacity) }
+        Self { granule, frames: Vec::with_capacity(capacity) }
     }
 
     /// Gets from the global allocator a granule-aligned region that suits `hint` and share it.
     fn refill(&mut self, pool: &mut FrameAllocator<32>, hint: Layout) {
         let layout = hint.align_to(self.granule).unwrap().pad_to_align();
         assert_ne!(layout.size(), 0);
-        // SAFETY - layout has non-zero size.
+        // SAFETY: layout has non-zero size.
         let Some(shared) = NonNull::new(unsafe { alloc_zeroed(layout) }) else {
             handle_alloc_error(layout);
         };
 
         let base = shared.as_ptr() as usize;
         let end = base.checked_add(layout.size()).unwrap();
-        trace!("Sharing memory region {:#x?}", base..end);
-        for vaddr in (base..end).step_by(self.granule) {
-            let vaddr = NonNull::new(vaddr as *mut _).unwrap();
-            get_hypervisor().mem_share(virt_to_phys(vaddr).try_into().unwrap()).unwrap();
-        }
-        self.shared_regions.push((base, layout));
 
+        if let Some(mem_sharer) = get_mem_sharer() {
+            trace!("Sharing memory region {:#x?}", base..end);
+            for vaddr in (base..end).step_by(self.granule) {
+                let vaddr = NonNull::new(vaddr as *mut _).unwrap();
+                mem_sharer.share(virt_to_phys(vaddr).try_into().unwrap()).unwrap();
+            }
+        }
+
+        self.frames.push((base, layout));
         pool.add_frame(base, end);
     }
 }
 
 impl Drop for MemorySharer {
     fn drop(&mut self) {
-        while let Some((base, layout)) = self.shared_regions.pop() {
-            let end = base.checked_add(layout.size()).unwrap();
-            trace!("Unsharing memory region {:#x?}", base..end);
-            for vaddr in (base..end).step_by(self.granule) {
-                let vaddr = NonNull::new(vaddr as *mut _).unwrap();
-                get_hypervisor().mem_unshare(virt_to_phys(vaddr).try_into().unwrap()).unwrap();
+        while let Some((base, layout)) = self.frames.pop() {
+            if let Some(mem_sharer) = get_mem_sharer() {
+                let end = base.checked_add(layout.size()).unwrap();
+                trace!("Unsharing memory region {:#x?}", base..end);
+                for vaddr in (base..end).step_by(self.granule) {
+                    let vaddr = NonNull::new(vaddr as *mut _).unwrap();
+                    mem_sharer.unshare(virt_to_phys(vaddr).try_into().unwrap()).unwrap();
+                }
             }
 
-            // SAFETY - The region was obtained from alloc_zeroed() with the recorded layout.
+            // SAFETY: The region was obtained from alloc_zeroed() with the recorded layout.
             unsafe { dealloc(base as *mut _, layout) };
         }
     }
@@ -448,9 +491,25 @@
         // Since mmio_guard_map takes IPAs, if pvmfw moves non-ID address mapping, page_base
         // should be converted to IPA. However, since 0x0 is a valid MMIO address, we don't use
         // virt_to_phys here, and just pass page_base instead.
-        get_hypervisor().mmio_guard_unmap(page_base).map_err(|e| {
+        get_mmio_guard().unwrap().unmap(page_base).map_err(|e| {
             error!("Error MMIO guard unmapping: {e}");
         })?;
     }
     Ok(())
 }
+
+/// Handles a translation fault with the given fault address register (FAR).
+#[inline]
+pub fn handle_translation_fault(far: VirtualAddress) -> result::Result<(), HandleExceptionError> {
+    let mut guard = MEMORY.try_lock().ok_or(HandleExceptionError::PageTableUnavailable)?;
+    let memory = guard.as_mut().ok_or(HandleExceptionError::PageTableNotInitialized)?;
+    Ok(memory.handle_mmio_fault(far)?)
+}
+
+/// Handles a permission fault with the given fault address register (FAR).
+#[inline]
+pub fn handle_permission_fault(far: VirtualAddress) -> result::Result<(), HandleExceptionError> {
+    let mut guard = MEMORY.try_lock().ok_or(HandleExceptionError::PageTableUnavailable)?;
+    let memory = guard.as_mut().ok_or(HandleExceptionError::PageTableNotInitialized)?;
+    Ok(memory.handle_permission_fault(far)?)
+}
diff --git a/vmbase/src/memory/util.rs b/vmbase/src/memory/util.rs
index b9ef5c9..2b75414 100644
--- a/vmbase/src/memory/util.rs
+++ b/vmbase/src/memory/util.rs
@@ -55,7 +55,7 @@
     let start = unchecked_align_down(start, line_size);
 
     for line in (start..end).step_by(line_size) {
-        // SAFETY - Clearing cache lines shouldn't have Rust-visible side effects.
+        // SAFETY: Clearing cache lines shouldn't have Rust-visible side effects.
         unsafe {
             asm!(
                 "dc cvau, {x}",
@@ -88,7 +88,7 @@
 ///
 /// As we use identity mapping for everything, this is just a cast, but it's useful to use it to be
 /// explicit about where we are converting from virtual to physical address.
-pub fn virt_to_phys(vaddr: NonNull<u8>) -> usize {
+pub(crate) fn virt_to_phys(vaddr: NonNull<u8>) -> usize {
     vaddr.as_ptr() as _
 }
 
@@ -96,6 +96,6 @@
 /// physical address.
 ///
 /// Panics if `paddr` is 0.
-pub fn phys_to_virt(paddr: usize) -> NonNull<u8> {
+pub(crate) fn phys_to_virt(paddr: usize) -> NonNull<u8> {
     NonNull::new(paddr as _).unwrap()
 }
diff --git a/vmbase/src/rand.rs b/vmbase/src/rand.rs
index 00567b8..2acc390 100644
--- a/vmbase/src/rand.rs
+++ b/vmbase/src/rand.rs
@@ -17,13 +17,29 @@
 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 {
+    /// No source of entropy found.
+    NoEntropySource,
+    /// Error during architectural SMCCC call.
+    Smccc(smccc::arch::Error),
     /// Error during SMCCC TRNG call.
     Trng(hvc::trng::Error),
+    /// Unsupported SMCCC version.
+    UnsupportedSmcccVersion(smccc::arch::Version),
     /// Unsupported SMCCC TRNG version.
-    UnsupportedVersion((u16, u16)),
+    UnsupportedTrngVersion(hvc::trng::Version),
+}
+
+impl From<smccc::arch::Error> for Error {
+    fn from(e: smccc::arch::Error) -> Self {
+        Self::Smccc(e)
+    }
 }
 
 impl From<hvc::trng::Error> for Error {
@@ -38,10 +54,11 @@
 impl fmt::Display for Error {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         match self {
+            Self::NoEntropySource => write!(f, "No source of entropy available"),
+            Self::Smccc(e) => write!(f, "Architectural SMCCC error: {e}"),
             Self::Trng(e) => write!(f, "SMCCC TRNG error: {e}"),
-            Self::UnsupportedVersion((x, y)) => {
-                write!(f, "Unsupported SMCCC TRNG version v{x}.{y}")
-            }
+            Self::UnsupportedSmcccVersion(v) => write!(f, "Unsupported SMCCC version {v}"),
+            Self::UnsupportedTrngVersion(v) => write!(f, "Unsupported SMCCC TRNG version {v}"),
         }
     }
 }
@@ -53,53 +70,83 @@
 }
 
 /// Configure the source of entropy.
-pub fn init() -> Result<()> {
-    match hvc::trng_version()? {
-        (1, _) => Ok(()),
-        version => Err(Error::UnsupportedVersion(version)),
+pub(crate) fn init() -> Result<()> {
+    // SMCCC TRNG requires SMCCC v1.1.
+    match smccc::arch::version::<Hvc>()? {
+        smccc::arch::Version { major: 1, minor } if minor >= 1 => (),
+        version => return Err(Error::UnsupportedSmcccVersion(version)),
     }
+
+    // TRNG_RND requires SMCCC TRNG v1.0.
+    match hvc::trng_version()? {
+        hvc::trng::Version { major: 1, minor: _ } => (),
+        version => return Err(Error::UnsupportedTrngVersion(version)),
+    }
+
+    // TRNG_RND64 doesn't define any special capabilities so ignore the successful result.
+    let _ = hvc::trng_features(hvc::ARM_SMCCC_TRNG_RND64).map_err(|e| {
+        if e == hvc::trng::Error::NotSupported {
+            // SMCCC TRNG is currently our only source of entropy.
+            Error::NoEntropySource
+        } else {
+            e.into()
+        }
+    })?;
+
+    Ok(())
 }
 
-fn fill_with_entropy(s: &mut [u8]) -> Result<()> {
-    const MAX_BYTES_PER_CALL: usize = size_of::<hvc::TrngRng64Entropy>();
+/// 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::<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) -> hvc::trng::Result<hvc::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) {
-            Err(hvc::trng::Error::NoEntropy) => continue,
-            res => return res,
+        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];
@@ -114,7 +161,7 @@
 
 #[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.
+    // 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) };
     fill_with_entropy(s).unwrap()
 }
diff --git a/vmbase/src/uart.rs b/vmbase/src/uart.rs
index 0fc2494..09d747f 100644
--- a/vmbase/src/uart.rs
+++ b/vmbase/src/uart.rs
@@ -38,8 +38,8 @@
 
     /// Writes a single byte to the UART.
     pub fn write_byte(&self, byte: u8) {
-        // Safe because we know that the base address points to the control registers of an UART
-        // device which is appropriately mapped.
+        // SAFETY: We know that the base address points to the control registers of a UART device
+        // which is appropriately mapped.
         unsafe {
             write_volatile(self.base_address, byte);
         }
@@ -55,5 +55,5 @@
     }
 }
 
-// Safe because it just contains a pointer to device memory, which can be accessed from any context.
+// SAFETY: `Uart` just contains a pointer to device memory, which can be accessed from any context.
 unsafe impl Send for Uart {}
diff --git a/vmbase/src/virtio/hal.rs b/vmbase/src/virtio/hal.rs
index 36f9e56..0d3f445 100644
--- a/vmbase/src/virtio/hal.rs
+++ b/vmbase/src/virtio/hal.rs
@@ -32,10 +32,8 @@
 /// HAL implementation for the virtio_drivers crate.
 pub struct HalImpl;
 
-/// # Safety
-///
-/// See the 'Implementation Safety' comments on methods below for how they fulfill the safety
-/// requirements of the unsafe `Hal` trait.
+/// SAFETY: See the 'Implementation Safety' comments on methods below for how they fulfill the
+/// safety requirements of the unsafe `Hal` trait.
 unsafe impl Hal for HalImpl {
     /// # Implementation Safety
     ///
@@ -48,14 +46,14 @@
         let layout = dma_layout(pages);
         let vaddr =
             alloc_shared(layout).expect("Failed to allocate and share VirtIO DMA range with host");
-        // SAFETY - vaddr points to a region allocated for the caller so is safe to access.
+        // SAFETY: vaddr points to a region allocated for the caller so is safe to access.
         unsafe { core::ptr::write_bytes(vaddr.as_ptr(), 0, layout.size()) };
         let paddr = virt_to_phys(vaddr);
         (paddr, vaddr)
     }
 
     unsafe fn dma_dealloc(_paddr: PhysAddr, vaddr: NonNull<u8>, pages: usize) -> i32 {
-        // SAFETY - Memory was allocated by `dma_alloc` using `alloc_shared` with the same layout.
+        // SAFETY: Memory was allocated by `dma_alloc` using `alloc_shared` with the same layout.
         unsafe { dealloc_shared(vaddr, dma_layout(pages)) }
             .expect("Failed to unshare VirtIO DMA range with host");
         0
@@ -68,7 +66,7 @@
     /// range. It can't alias any other allocations because we previously validated in
     /// `map_mmio_range` that the PCI MMIO range didn't overlap with any other memory ranges.
     unsafe fn mmio_phys_to_virt(paddr: PhysAddr, size: usize) -> NonNull<u8> {
-        let pci_info = PCI_INFO.get().expect("VirtIO HAL used before PCI_INFO was initialised");
+        let pci_info = PCI_INFO.get().expect("VirtIO HAL used before PCI_INFO was initialized");
         let bar_range = {
             let start = pci_info.bar_range.start.try_into().unwrap();
             let end = pci_info.bar_range.end.try_into().unwrap();
@@ -96,7 +94,7 @@
         if direction == BufferDirection::DriverToDevice {
             let src = buffer.cast::<u8>().as_ptr().cast_const();
             trace!("VirtIO bounce buffer at {bounce:?} (PA:{paddr:#x}) initialized from {src:?}");
-            // SAFETY - Both regions are valid, properly aligned, and don't overlap.
+            // SAFETY: Both regions are valid, properly aligned, and don't overlap.
             unsafe { copy_nonoverlapping(src, bounce.as_ptr(), size) };
         }
 
@@ -109,11 +107,11 @@
         if direction == BufferDirection::DeviceToDriver {
             let dest = buffer.cast::<u8>().as_ptr();
             trace!("VirtIO bounce buffer at {bounce:?} (PA:{paddr:#x}) copied back to {dest:?}");
-            // SAFETY - Both regions are valid, properly aligned, and don't overlap.
+            // SAFETY: Both regions are valid, properly aligned, and don't overlap.
             unsafe { copy_nonoverlapping(bounce.as_ptr(), dest, size) };
         }
 
-        // SAFETY - Memory was allocated by `share` using `alloc_shared` with the same layout.
+        // SAFETY: Memory was allocated by `share` using `alloc_shared` with the same layout.
         unsafe { dealloc_shared(bounce, bb_layout(size)) }
             .expect("Failed to unshare and deallocate VirtIO bounce buffer");
     }
diff --git a/vmbase/src/virtio/mod.rs b/vmbase/src/virtio/mod.rs
index df916bc..fbe41e3 100644
--- a/vmbase/src/virtio/mod.rs
+++ b/vmbase/src/virtio/mod.rs
@@ -16,3 +16,5 @@
 
 mod hal;
 pub mod pci;
+
+pub use hal::HalImpl;
diff --git a/vmbase/src/virtio/pci.rs b/vmbase/src/virtio/pci.rs
index cbb4d26..1d05c18 100644
--- a/vmbase/src/virtio/pci.rs
+++ b/vmbase/src/virtio/pci.rs
@@ -14,19 +14,20 @@
 
 //! Functions to scan the PCI bus for VirtIO devices.
 
-use super::hal::HalImpl;
 use crate::memory::{MemoryTracker, MemoryTrackerError};
 use alloc::boxed::Box;
 use core::fmt;
+use core::marker::PhantomData;
 use fdtpci::PciInfo;
 use log::debug;
 use once_cell::race::OnceBox;
 use virtio_drivers::{
-    device::blk,
+    device::{blk, socket},
     transport::pci::{
         bus::{BusDeviceIterator, PciRoot},
         virtio_device_type, PciTransport,
     },
+    Hal,
 };
 
 pub(super) static PCI_INFO: OnceBox<PciInfo> = OnceBox::new();
@@ -63,7 +64,7 @@
 /// 3. Creates and returns a `PciRoot`.
 ///
 /// This must only be called once; it will panic if it is called a second time.
-pub fn initialise(pci_info: PciInfo, memory: &mut MemoryTracker) -> Result<PciRoot, PciError> {
+pub fn initialize(pci_info: PciInfo, memory: &mut MemoryTracker) -> Result<PciRoot, PciError> {
     PCI_INFO.set(Box::new(pci_info.clone())).map_err(|_| PciError::DuplicateInitialization)?;
 
     memory.map_mmio_range(pci_info.cam_range.clone()).map_err(PciError::CamMapFailed)?;
@@ -76,23 +77,29 @@
 }
 
 /// Virtio Block device.
-pub type VirtIOBlk = blk::VirtIOBlk<HalImpl, PciTransport>;
+pub type VirtIOBlk<T> = blk::VirtIOBlk<T, PciTransport>;
+
+/// Virtio Socket device.
+///
+/// Spec: https://docs.oasis-open.org/virtio/virtio/v1.2/csd01/virtio-v1.2-csd01.html 5.10
+pub type VirtIOSocket<T> = socket::VirtIOSocket<T, PciTransport>;
 
 /// An iterator that iterates over the PCI transport for each device.
-pub struct PciTransportIterator<'a> {
+pub struct PciTransportIterator<'a, T: Hal> {
     pci_root: &'a mut PciRoot,
     bus: BusDeviceIterator,
+    _hal: PhantomData<T>,
 }
 
-impl<'a> PciTransportIterator<'a> {
+impl<'a, T: Hal> PciTransportIterator<'a, T> {
     /// Creates a new iterator.
     pub fn new(pci_root: &'a mut PciRoot) -> Self {
         let bus = pci_root.enumerate_bus(0);
-        Self { pci_root, bus }
+        Self { pci_root, bus, _hal: PhantomData }
     }
 }
 
-impl<'a> Iterator for PciTransportIterator<'a> {
+impl<'a, T: Hal> Iterator for PciTransportIterator<'a, T> {
     type Item = PciTransport;
 
     fn next(&mut self) -> Option<Self::Item> {
@@ -109,7 +116,7 @@
             };
             debug!("  VirtIO {:?}", virtio_type);
 
-            return PciTransport::new::<HalImpl>(self.pci_root, device_function).ok();
+            return PciTransport::new::<T>(self.pci_root, device_function).ok();
         }
     }
 }
diff --git a/vmclient/src/lib.rs b/vmclient/src/lib.rs
index 8f25b99..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))) }
 }
 
@@ -175,14 +175,17 @@
     pub fn create(
         service: &dyn IVirtualizationService,
         config: &VirtualMachineConfig,
-        console: Option<File>,
+        console_out: Option<File>,
+        console_in: Option<File>,
         log: Option<File>,
         callback: Option<Box<dyn VmCallback + Send + Sync>>,
     ) -> BinderResult<Self> {
-        let console = console.map(ParcelFileDescriptor::new);
+        let console_out = console_out.map(ParcelFileDescriptor::new);
+        let console_in = console_in.map(ParcelFileDescriptor::new);
         let log = log.map(ParcelFileDescriptor::new);
 
-        let vm = service.createVm(config, console.as_ref(), log.as_ref())?;
+        let vm =
+            service.createVm(config, console_out.as_ref(), console_in.as_ref(), log.as_ref())?;
 
         let cid = vm.getCid()?;
 
diff --git a/zipfuse/src/inode.rs b/zipfuse/src/inode.rs
index ea63422..ef48389 100644
--- a/zipfuse/src/inode.rs
+++ b/zipfuse/src/inode.rs
@@ -210,7 +210,7 @@
                     parent = found;
                     // Update the mode if this is a directory leaf.
                     if !is_file && is_leaf {
-                        let mut inode = table.get_mut(parent).unwrap();
+                        let inode = table.get_mut(parent).unwrap();
                         inode.mode = file.unix_mode().unwrap_or(DEFAULT_DIR_MODE);
                     }
                     continue;