Merge changes I0f33b6c4,If748cf16 into main

* changes:
  Run tests on both pVMs and non-pVMS
  Don't run non-protected tests if not supported
diff --git a/README.md b/README.md
index eb28e94..eaa2579 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,29 @@
-# Virtualization
+# Android Virtualization Framework (AVF)
 
-This repository contains userspace services related to running virtual machines on Android,
-especially protected virtual machines. See the
-[getting started documentation](docs/getting_started/index.md) and
-[Microdroid README](microdroid/README.md) for more information.
+Android Virtualization Framework (AVF) provides secure and private execution environments for
+executing code. AVF is ideal for security-oriented use cases that require stronger isolation
+assurances over those offered by Android’s app sandbox.
+
+Visit [our public doc site](https://source.android.com/docs/core/virtualization) to learn more about
+what AVF is, what it is for, and how it is structured. This repository contains source code for
+userspace components of AVF.
+
+If you want a quick start, see the [getting started guideline](docs/getting_started.md)
+and follow the steps there.
+
+For in-depth explanations about individual topics and components, visit the following links.
+
+AVF components:
+
+* [pVM firmware](pvmfw/README.md)
+* [Microdroid](microdroid/README.md)
+* [Microdroid kernel](microdroid/kernel/README.md)
+* [Microdroid payload](microdroid/payload/README.md)
+* [vmbase](vmbase/README.md)
+* [VM Payload API](vm_payload/README.md)
+
+How-Tos:
+* [Building and running a demo app in Java](demo/README.md)
+* [Building and running a demo app in C++](demo_native/README.md)
+* [Debugging](docs/debug)
+* [Using custom VM](docs/custom_vm.md)
diff --git a/docs/custom_vm.md b/docs/custom_vm.md
new file mode 100644
index 0000000..270ea36
--- /dev/null
+++ b/docs/custom_vm.md
@@ -0,0 +1,23 @@
+# Custom VM
+
+You can spawn your own custom VMs by passing a JSON config file to the
+VirtualizationService via the `vm` tool on a rooted AVF-enabled device. If your
+device is attached over ADB, you can run:
+
+```shell
+cat > vm_config.json <<EOF
+{
+  "kernel": "/data/local/tmp/kernel",
+  "initrd": "/data/local/tmp/ramdisk",
+  "params": "rdinit=/bin/init"
+}
+EOF
+adb root
+adb push <kernel> /data/local/tmp/kernel
+adb push <ramdisk> /data/local/tmp/ramdisk
+adb push vm_config.json /data/local/tmp/vm_config.json
+adb shell "/apex/com.android.virt/bin/vm run /data/local/tmp/vm_config.json"
+```
+
+The `vm` command also has other subcommands for debugging; run
+`/apex/com.android.virt/bin/vm help` for details.
diff --git a/docs/debug/README.md b/docs/debug/README.md
new file mode 100644
index 0000000..b7729a4
--- /dev/null
+++ b/docs/debug/README.md
@@ -0,0 +1,119 @@
+# Debugging protected VMs
+
+AVF is largely about protected VMs. This in turn means that anything that is
+happening inside the VM cannot be observed from outside of the VM. But as a
+developer, you need to be able to look into it when there’s an error in your
+VM. To satisfy such contradictory needs, AVF allows you to start a protected VM
+in a debuggable mode and provides a bunch of debugging mechanisms you can use
+to better understand the behavior of the VM and diagnose issues.
+
+Note: running a protected VM in a debuggable mode introduces many loopholes
+which can be used to nullify the protection provided by the hypervisor.
+Therefore, the debugable mode should never be used in production.
+
+## Enable debugging
+
+The following sections describe the two ways debugging can be enabled.
+
+### Debug level
+
+Debug level is a per-VM property which indicates how debuggable the VM is.
+There currently are two levels defined: NONE and FULL. NONE means that the VM
+is not debuggable at all, and FULL means that [all the debugging
+features](#debugging-features) are supported.
+
+Debug level is by default NONE. You can set it to FULL either via a Java API
+call in your app or via a command line argument `--debug` as follows:
+
+```java
+VirtualMachineConfig.Builder.setDebugLevel(DEBUG_LEVEL_FULL);
+```
+
+or
+
+```shell
+adb shell /apex/com.android.virt/bin/vm run-microdroid --debug full
+```
+
+or
+
+```shell
+m vm_shell
+vm_shell start-microdroid --auto-connect -- --protected --debug full
+```
+
+Note: `--debug full` is the default option when omitted. You need to explicitly
+use `--debug none` to set the debug level to NONE.
+
+### Debug policy
+
+Debug policy is a per-device property which forcibly enables selected debugging
+features, even for the VMs with debug level NONE.
+
+The main purpose of debug policy is in-field debugging by the platform
+developers (device makers, SoC vendors, etc.) To understand it, let’s imagine
+that you have an application of pVM. It’s configured as debug level NONE
+because you finished the development and the team-level testing. However, you
+get a bug report from your QA team or from beta testers. To fix the bug, you
+should be able to look into the pVM but you do not want to change the source
+code to make the VM debuggable and rebuild the entire software, because that
+may hurt the reproducibility of the bug.
+
+Note: Not every devices is guaranteed to support debug policy. It is up to the
+device manufacturer to implement this in their bootloader. Google Pixel
+devices for example support this after Pixel 7 and 7 Pro. Pixel 6 and 6 Pro
+don't support debug policy.
+
+In the Pixel phones supporting debug policy, it is provisioned by installing a
+device tree overlay like below to the Pixel-specific partition `dpm`.
+
+```
+/ {
+    fragment@avf {
+        target-path = "/";
+
+        __overlay__ {
+            avf {
+                common {
+                    log = <1>; // Enable kernel log and logcat
+                    ramdump = <1>; // Enable ramdump
+                }
+                microdroid {
+                    adb = <1>; // Enable ADB connection
+                }
+            };
+        };
+    };
+}; /* end of avf */
+```
+
+To not enable a specific debugging feature, set the corresponding property
+value to other than `<1>`, or delete the property.
+
+As a reference, in Pixel phones, debug policy is loaded as below:
+
+1. Bootloader loads it from the `dpm` partition and verifies it.
+1. Bootloader appends the loaded debug policy as the [configuration
+   data](../../pvmfw/README.md#configuration-data) of the pvmfw.
+1. When a pVM is started, pvmfw [overlays][apply_debug_policy] the debug policy to the baseline
+   device tree from crosvm.
+1. OS payload (e.g. Microdroid) [reads][read_debug_policy] the device tree and enables specific
+   debugging feature accordingly.
+
+**Note**: Bootloader MUST NOT load debug policy when the bootloader is in LOCKED state.
+
+[apply_debug_policy]: https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Virtualization/pvmfw/src/fdt.rs;drc=0d52747770baa14d44c0779b5505095b4251f2e9;l=790
+[read_debug_policy]: https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Virtualization/microdroid_manager/src/main.rs;drc=65c9f1f0eee4375535f2025584646a0dbb0ea25c;l=834
+
+## Debugging features
+
+AVF currently supports the following debugging features:
+
+* ADB connection (only for Microdroid)
+* Capturing console output
+* Capturing logcat output (only for Microdroid)
+* [Capturing kernel ramdump](ramdump.md) (only for Microdroid)
+* Capturing userspace crash dump (only for Microdroid)
+* [Attaching GDB to the kernel](gdb_kernel.md)
+* [Attaching GDB to the userspace process](gdb_userspace.md) (only for Microdroid)
+* [Tracing hypervisor events](tracing.md)
diff --git a/docs/debug/gdb.md b/docs/debug/gdb_kernel.md
similarity index 100%
rename from docs/debug/gdb.md
rename to docs/debug/gdb_kernel.md
diff --git a/docs/debug/gdb_userspace.md b/docs/debug/gdb_userspace.md
new file mode 100644
index 0000000..c8af702
--- /dev/null
+++ b/docs/debug/gdb_userspace.md
@@ -0,0 +1,18 @@
+# Debugging the payload on microdroid
+
+Like a normal adb device, you can debug native processes running on a
+Microdroid-base VM using [`lldbclient.py`][lldbclient] script, either by
+running a new process, or attaching to an existing process.  Use `vm_shell`
+tool above, and then run `lldbclient.py`.
+
+```sh
+adb -s localhost:8000 shell 'mount -o remount,exec /data'
+development/scripts/lldbclient.py -s localhost:8000 --chroot . --user '' \
+    (-p PID | -n NAME | -r ...)
+```
+
+**Note:** We need to pass `--chroot .` to skip verifying device, because
+microdroid doesn't match with the host's lunch target. We need to also pass
+`--user ''` as there is no `su` binary in microdroid.
+
+[lldbclient]: https://android.googlesource.com/platform/development/+/refs/heads/main/scripts/lldbclient.py
diff --git a/docs/getting_started.md b/docs/getting_started.md
new file mode 100644
index 0000000..d970c12
--- /dev/null
+++ b/docs/getting_started.md
@@ -0,0 +1,156 @@
+# Getting started with Android Virtualization Framework
+
+## Step 1: Prepare a device
+
+We support the following devices:
+
+* aosp\_panther (Pixel 7)
+* aosp\_cheetah (Pixel 7 Pro)
+* aosp\_oriole (Pixel 6)
+* aosp\_raven (Pixel 6 Pro)
+* aosp\_felix (Pixel Fold)
+* aosp\_tangopro (Pixel Tablet)
+* aosp\_cf\_x86\_64\_phone (Cuttlefish a.k.a. Cloud Android). Follow [this
+  instruction](https://source.android.com/docs/setup/create/cuttlefish-use) to
+  use.
+
+### Note on Pixel 6 and 6 Pro
+AVF is shipped in Pixel 6 and 6 Pro, but isn't enabled by default. To enable
+it, follow the instructions below:
+
+1. If the device is running Android 13 or earlier, upgrade to Android 14.
+
+1. Once upgraded to Android 14, execute the following command to enable pKVM.
+   ```shell
+   adb reboot bootloader
+   fastboot flashing unlock
+   fastboot oem pkvm enable
+   fastboot reboot
+   ```
+### Note on Cuttlefish
+Cuttlefish does not support protected VMs. Only non-protected VMs are
+supported.
+
+## Step 2: Build Android image
+
+This step is optional unless you want to build AVF by yourself or try the
+in-development version of AVF.
+
+AVF is implemented as an APEX named `com.android.virt`. However, in order for
+you to install it to your device (be it Pixel or Cuttlefish), you first need to
+re-build the entire Android from AOSP. This is because the official Android
+build you have in your device is release-key signed and therefore you can't
+install your custom-built AVF APEX to it - because it is test-key signed.
+
+### Pixel
+
+1. [Download](https://source.android.com/docs/setup/download/downloading)
+   source code from AOSP. Use the `main` branch.
+
+1. [Download](https://developers.google.com/android/blobs-preview) the preview
+   vendor blob that matches your device.
+
+1. [Build](https://source.android.com/docs/setup/build/building) the `aosp_`
+   variant of your device. For example, if your device is Pixel 7 (`panther`),
+   build `aosp_panther`.
+
+1. [Flash](https://source.android.com/docs/setup/build/running) the built
+   images to the device.
+
+
+### Cuttlefish
+
+1. [Download](https://source.android.com/docs/setup/download/downloading)
+   source code from AOSP. Use the `main` branch.
+
+1. Build Cuttlefish:
+   ```shell
+   source build/envsetup.sh
+   lunch aosp_cf_x86_64_phone-userdebug
+   m
+   ```
+
+1. Run Cuttlefish:
+   ```shell
+   cvd start
+   ```
+
+## Step 3: Build AVF
+
+Then you can repeat building and installing AVF to the device as follows:
+
+1. Build the AVF APEX.
+   ```sh
+   banchan com.android.virt aosp_arm64
+   UNBUNDLED_BUILD_SDKS_FROM_SOURCE=true m apps_only dist
+   ```
+   Replace `aosp_arm64` with `aosp_x86_64` if you are building for Cuttlefish.
+
+1. Install the AVF APEX to the device.
+   ```sh
+   adb install out/dist/com.android.virt.apex
+   adb reboot; adb wait-for-device
+   ```
+
+## Step 4: Run a Microdroid VM
+
+[Microdroid](../../microdroid/README.md) is a lightweight version of Android
+that is intended to run on pVM. You can run a Microdroid-based VM with an empty
+payload using the following command:
+
+```shell
+package/modules/Virtualization/vm/vm_shell.sh start-microdroid --auto-connect -- --protected
+```
+
+You will see the log messages like the below.
+
+```
+found path /apex/com.android.virt/app/EmptyPayloadAppGoogle@MASTER/EmptyPayloadAppGoogle.apk
+creating work dir /data/local/tmp/microdroid/7CI6QtktSluD3OZgv
+apk.idsig path: /data/local/tmp/microdroid/7CI6QtktSluD3OZgv/apk.idsig
+instance.img path: /data/local/tmp/microdroid/7CI6QtktSluD3OZgv/instance.img
+Created VM from "/apex/com.android.virt/app/EmptyPayloadAppGoogle@MASTER/EmptyPayloadAppGoogle.apk"!PayloadConfig(VirtualMachinePayloadConfig { payloadBinaryName: "MicrodroidEmptyPayloadJniLib.so" }) with CID 2052, state is STARTING.
+[2023-07-07T14:50:43.420766770+09:00 INFO  crosvm] crosvm started.
+[2023-07-07T14:50:43.422545090+09:00 INFO  crosvm] CLI arguments parsed.
+[2023-07-07T14:50:43.440984015+09:00 INFO  crosvm::crosvm::sys::unix::device_helpers] Trying to attach block device: /proc/self/fd/49
+[2023-07-07T14:50:43.441730922+09:00 INFO  crosvm::crosvm::sys::unix::device_helpers] Trying to attach block device: /proc/self/fd/54
+[2023-07-07T14:50:43.462055141+09:00 INFO  crosvm::crosvm::sys::unix::device_helpers] Trying to attach block device: /proc/self/fd/63
+[WARN] Config entry DebugPolicy uses non-zero offset with zero size
+[WARN] Config entry DebugPolicy uses non-zero offset with zero size
+[INFO] pVM firmware
+avb_slot_verify.c:443: ERROR: initrd_normal: Hash of data does not match digest in descriptor.
+[INFO] device features: SEG_MAX | RO | BLK_SIZE | RING_EVENT_IDX | VERSION_1 | ACCESS_PLATFORM
+[INFO] config: 0x201a000
+[INFO] found a block device of size 50816KB
+[INFO] device features: SEG_MAX | BLK_SIZE | FLUSH | DISCARD | WRITE_ZEROES | RING_EVENT_IDX | VERSION_1 | ACCESS_PLATFORM
+[INFO] config: 0x2022000
+[INFO] found a block device of size 10304KB
+[INFO] No debug policy found.
+[INFO] Starting payload...
+<omitted>
+07-07 05:52:01.322    69    69 I vm_payload: vm_payload: Notified host payload ready successfully
+07-07 05:52:01.364    70    70 I adbd    : persist.adb.watchdog set to ''
+07-07 05:52:01.364    70    70 I adbd    : persist.sys.test_harness set to ''
+07-07 05:52:01.365    70    70 I adbd    : adb watchdog timeout set to 600 seconds
+07-07 05:52:01.366    70    70 I adbd    : Setup mdns on port= 5555
+07-07 05:52:01.366    70    70 I adbd    : adbd listening on vsock:5555
+07-07 05:52:01.366    70    70 I adbd    : adbd started
+#
+```
+
+The `--auto-connect` option provides you an adb-shell connection to the VM. The
+shell promot (`#`) at the end of the log is for that.
+
+## Step 5: Run tests
+
+There are various tests that spawn guest VMs and check different aspects of the
+architecture. They all can run via `atest`.
+
+```shell
+atest MicrodroidHostTestCases
+atest MicrodroidTestApp
+```
+
+If you run into problems, inspect the logs produced by `atest`. Their location
+is printed at the end. The `host_log_*.zip` file should contain the output of
+individual commands as well as VM logs.
diff --git a/docs/getting_started/index.md b/docs/getting_started/index.md
deleted file mode 100644
index 9dcd4fa..0000000
--- a/docs/getting_started/index.md
+++ /dev/null
@@ -1,126 +0,0 @@
-# Getting started with Protected Virtual Machines
-
-## Prepare a device
-
-First you will need a device that is capable of running virtual machines. On arm64, this means a
-device which boots the kernel in EL2 and the kernel was built with KVM enabled. Unfortunately at the
-moment, we don't have an arm64 device in AOSP which does that. Instead, use cuttlefish which
-provides the same functionalities except that the virtual machines are not protected from the host
-(i.e. Android). This however should be enough for functional testing.
-
-We support the following device:
-
-* aosp_cf_x86_64_phone (Cuttlefish a.k.a. Cloud Android)
-* oriole/raven (Pixel 6, and 6 Pro)
-* panther/cheetah (Pixel 7, and 7 Pro)
-
-### Cuttlefish
-
-Building Cuttlefish
-
-```shell
-source build/envsetup.sh
-lunch aosp_cf_x86_64_phone-userdebug
-m
-```
-
-Run Cuttlefish locally by
-
-```shell
-acloud create --local-instance --local-image
-```
-
-### Google Pixel phones
-
-If the device is running Android 13 or earlier, join the [Android Beta
-Program](https://developer.android.com/about/versions/14/get#on_pixel) to upgrade to Android 14
-Beta.
-
-Once upgraded to Android 14, and if you are using Pixel 6 or 6 Pro, execute the following command to
-enable pKVM. You don't need to do this for Pixel 7 and 7 Pro.
-
-```shell
-adb reboot bootloader
-fastboot flashing unlock
-fastboot oem pkvm enable
-fastboot reboot
-```
-
-## Running demo app
-
-The instruction is [here](../../demo/README.md).
-
-## Running tests
-
-There are various tests that spawn guest VMs and check different aspects of the architecture. They
-all can run via `atest`.
-
-```shell
-atest MicrodroidHostTestCases
-atest MicrodroidTestApp
-```
-
-If you run into problems, inspect the logs produced by `atest`. Their location is printed at the
-end. The `host_log_*.zip` file should contain the output of individual commands as well as VM logs.
-
-## Spawning your own VMs with custom kernel
-
-You can spawn your own VMs by passing a JSON config file to the VirtualizationService via the `vm`
-tool on a rooted KVM-enabled device. If your device is attached over ADB, you can run:
-
-```shell
-cat > vm_config.json
-{
-  "kernel": "/data/local/tmp/kernel",
-  "initrd": "/data/local/tmp/ramdisk",
-  "params": "rdinit=/bin/init"
-}
-adb root
-adb push <kernel> /data/local/tmp/kernel
-adb push <ramdisk> /data/local/tmp/ramdisk
-adb push vm_config.json /data/local/tmp/vm_config.json
-adb shell "start virtualizationservice"
-adb shell "/apex/com.android.virt/bin/vm run /data/local/tmp/vm_config.json"
-```
-
-The `vm` command also has other subcommands for debugging; run `/apex/com.android.virt/bin/vm help`
-for details.
-
-## Spawning your own VMs with custom pvmfw
-
-Set system property `hypervisor.pvmfw.path` to custom `pvmfw` on the device before using `vm` tool.
-`virtualizationservice` will pass the specified `pvmfw` to `crosvm` for protected VMs.
-
-```shell
-adb push pvmfw.img /data/local/tmp/pvmfw.img
-adb root  # required for setprop
-adb shell setprop hypervisor.pvmfw.path /data/local/tmp/pvmfw.img
-```
-
-## Spawning your own VMs with Microdroid
-
-[Microdroid](../../microdroid/README.md) is a lightweight version of Android that is intended to run
-on pVM. You can run a Microdroid with empty payload using the following command:
-
-```shell
-adb shell /apex/com.android.virt/bin/vm run-microdroid
-```
-
-which spawns a "debuggable" VM by default to allow access to guest kernel logs.
-To run a production non-debuggable VM, pass `--debug none`.
-
-## Building and updating CrosVM and VirtualizationService {#building-and-updating}
-
-You can update CrosVM and the VirtualizationService by updating the `com.android.virt` APEX instead
-of rebuilding the entire image.
-
-```shell
-banchan com.android.virt aosp_arm64   // or aosp_x86_64 if the device is cuttlefish
-UNBUNDLED_BUILD_SDKS_FROM_SOURCE=true m apps_only dist
-adb install out/dist/com.android.virt.apex
-adb reboot
-```
-
-## Building and updating kernel inside Microdroid
-
-The instruction is [here](../../microdroid/kernel/README.md).
diff --git a/javalib/api/test-current.txt b/javalib/api/test-current.txt
index 1298000..cf95770 100644
--- a/javalib/api/test-current.txt
+++ b/javalib/api/test-current.txt
@@ -13,6 +13,7 @@
 
   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/VirtualMachineConfig.java b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
index b400eeb..4cad2e3 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -76,6 +76,7 @@
     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)
@@ -167,6 +168,8 @@
     /** 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,
@@ -178,7 +181,8 @@
             @CpuTopology int cpuTopology,
             long encryptedStorageBytes,
             boolean vmOutputCaptured,
-            boolean vmConsoleInputSupported) {
+            boolean vmConsoleInputSupported,
+            @Nullable File vendorDiskImage) {
         // This is only called from Builder.build(); the builder handles parameter validation.
         mPackageName = packageName;
         mApkPath = apkPath;
@@ -191,6 +195,7 @@
         mEncryptedStorageBytes = encryptedStorageBytes;
         mVmOutputCaptured = vmOutputCaptured;
         mVmConsoleInputSupported = vmConsoleInputSupported;
+        mVendorDiskImage = vendorDiskImage;
     }
 
     /** Loads a config from a file. */
@@ -267,6 +272,11 @@
         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();
     }
 
@@ -302,6 +312,9 @@
         }
         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);
     }
 
@@ -501,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;
     }
 
@@ -572,6 +599,7 @@
         private long mEncryptedStorageBytes;
         private boolean mVmOutputCaptured = false;
         private boolean mVmConsoleInputSupported = false;
+        @Nullable private File mVendorDiskImage;
 
         /**
          * Creates a builder for the given context.
@@ -645,7 +673,8 @@
                     mCpuTopology,
                     mEncryptedStorageBytes,
                     mVmOutputCaptured,
-                    mVmConsoleInputSupported);
+                    mVmConsoleInputSupported,
+                    mVendorDiskImage);
         }
 
         /**
@@ -863,5 +892,18 @@
             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/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 797138c..d9f0891 100644
--- a/pvmfw/src/exceptions.rs
+++ b/pvmfw/src/exceptions.rs
@@ -14,133 +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),
     }
 }
 
-/// Prints the details of an exception failure, excluding UART exceptions.
-#[inline]
-fn print_exception_failure(
-    esr: Esr,
-    far: usize,
-    elr: u64,
-    e: HandleExceptionError,
-    exception_name: &str,
-) {
-    let is_uart_exception = esr == Esr::DataAbortSyncExternalAbort && page_4kb_of(far) == UART_PAGE;
-    // Don't print to the UART if we are handling an exception it could raise.
-    if !is_uart_exception {
-        eprintln!("{exception_name}");
-        eprintln!("{e}");
-        eprintln!("{esr}, far={far:#08x}, elr={elr:#08x}");
-    }
-}
-
 #[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) {
-        print_exception_failure(esr, far, elr, e, "sync_exception_current");
+    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 319100f..244b192 100644
--- a/pvmfw/src/fdt.rs
+++ b/pvmfw/src/fdt.rs
@@ -209,7 +209,7 @@
 impl PciInfo {
     const IRQ_MASK_CELLS: usize = 4;
     const IRQ_MAP_CELLS: usize = 10;
-    const MAX_IRQS: usize = 8;
+    const MAX_IRQS: usize = 10;
 }
 
 type PciAddrRange = AddressRange<(u32, u64), u64, u64>;
@@ -248,14 +248,22 @@
     let range1 = ranges.next().ok_or(FdtError::NotFound)?;
 
     let irq_masks = node.getprop_cells(cstr!("interrupt-map-mask"))?.ok_or(FdtError::NotFound)?;
-    let irq_masks = CellChunkIterator::<{ PciInfo::IRQ_MASK_CELLS }>::new(irq_masks);
-    let irq_masks: ArrayVec<[PciIrqMask; PciInfo::MAX_IRQS]> =
-        irq_masks.take(PciInfo::MAX_IRQS).collect();
+    let mut chunks = CellChunkIterator::<{ PciInfo::IRQ_MASK_CELLS }>::new(irq_masks);
+    let irq_masks = (&mut chunks).take(PciInfo::MAX_IRQS).collect();
+
+    if chunks.next().is_some() {
+        warn!("Input DT has more than {} PCI entries!", PciInfo::MAX_IRQS);
+        return Err(FdtError::NoSpace);
+    }
 
     let irq_maps = node.getprop_cells(cstr!("interrupt-map"))?.ok_or(FdtError::NotFound)?;
-    let irq_maps = CellChunkIterator::<{ PciInfo::IRQ_MAP_CELLS }>::new(irq_maps);
-    let irq_maps: ArrayVec<[PciIrqMap; PciInfo::MAX_IRQS]> =
-        irq_maps.take(PciInfo::MAX_IRQS).collect();
+    let mut chunks = CellChunkIterator::<{ PciInfo::IRQ_MAP_CELLS }>::new(irq_maps);
+    let irq_maps = (&mut chunks).take(PciInfo::MAX_IRQS).collect();
+
+    if chunks.next().is_some() {
+        warn!("Input DT has more than {} PCI entries!", PciInfo::MAX_IRQS);
+        return Err(FdtError::NoSpace);
+    }
 
     Ok(PciInfo { ranges: [range0, range1], irq_masks, irq_maps })
 }
@@ -559,7 +567,7 @@
         *v = v.to_be();
     }
 
-    // SAFETY - array size is the same
+    // SAFETY: array size is the same
     let value = unsafe {
         core::mem::transmute::<
             [u32; NUM_INTERRUPTS * CELLS_PER_INTERRUPT],
@@ -801,7 +809,7 @@
         }
     };
 
-    // SAFETY - on failure, the corrupted DT is restored using the backup.
+    // SAFETY: on failure, the corrupted DT is restored using the backup.
     if let Err(e) = unsafe { fdt.apply_overlay(overlay) } {
         warn!("Failed to apply debug policy: {e}. Recovering...");
         fdt.copy_from_slice(backup_fdt.as_slice())?;
@@ -813,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);
@@ -823,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/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/src/error.rs b/rialto/src/error.rs
index 84228c4..0c1e25d 100644
--- a/rialto/src/error.rs
+++ b/rialto/src/error.rs
@@ -29,8 +29,6 @@
     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.
@@ -39,6 +37,10 @@
     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 {
@@ -48,11 +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 61c985e..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,
+    virtio::{
+        pci::{self, PciTransportIterator, VirtIOSocket},
+        HalImpl,
+    },
 };
 
 fn new_page_table() -> Result<PageTable> {
     let mut page_table = PageTable::default();
 
-    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:
@@ -98,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)
         })?;
@@ -113,60 +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 pci_root = pci::initialize(pci_info, MEMORY.lock().as_mut().unwrap())
+    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/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/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/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 ffb2c11..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"));
@@ -1934,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"})
@@ -2009,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/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/virtualizationmanager/src/aidl.rs b/virtualizationmanager/src/aidl.rs
index dd74d55..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::{
@@ -579,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,
@@ -620,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 {
@@ -1086,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 {
@@ -1349,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 8c412f6..31db3f6 100644
--- a/virtualizationmanager/src/crosvm.rs
+++ b/virtualizationmanager/src/crosvm.rs
@@ -592,7 +592,7 @@
     }
 
     let guest_time_ticks = data_list[42].parse::<i64>()?;
-    // SAFETY : It just returns an integer about CPU tick information.
+    // SAFETY: It just returns an integer about CPU tick information.
     let ticks_per_sec = unsafe { sysconf(_SC_CLK_TCK) };
     Ok(guest_time_ticks * MILLIS_PER_SEC / ticks_per_sec)
 }
@@ -910,8 +910,9 @@
 /// Creates a new pipe with the `O_CLOEXEC` flag set, and returns the read side and write side.
 fn create_pipe() -> Result<(File, File), Error> {
     let (raw_read, raw_write) = pipe2(OFlag::O_CLOEXEC)?;
-    // SAFETY: We are the sole owners of these fds as they were just created.
+    // SAFETY: We are the sole owner of this FD as we just created it, and it is valid and open.
     let read_fd = unsafe { File::from_raw_fd(raw_read) };
+    // SAFETY: We are the sole owner of this FD as we just created it, and it is valid and open.
     let write_fd = unsafe { File::from_raw_fd(raw_write) };
     Ok((read_fd, write_fd))
 }
diff --git a/virtualizationmanager/src/debug_config.rs b/virtualizationmanager/src/debug_config.rs
index 7172e7d..9b13475 100644
--- a/virtualizationmanager/src/debug_config.rs
+++ b/virtualizationmanager/src/debug_config.rs
@@ -42,7 +42,7 @@
     }
 
     fn to_path(&self) -> PathBuf {
-        // SAFETY -- unwrap() is safe for to_str() because node_path and prop_name were &str.
+        // unwrap() is safe for to_str() because node_path and prop_name were &str.
         PathBuf::from(
             [
                 "/sys/firmware/devicetree/base",
@@ -129,7 +129,7 @@
                 .map_err(Error::msg)
                 .with_context(|| "Malformed {overlay_file_path:?}")?;
 
-            // SAFETY - Return immediately if error happens. Damaged fdt_buf and fdt are discarded.
+            // SAFETY: Return immediately if error happens. Damaged fdt_buf and fdt are discarded.
             unsafe {
                 fdt.apply_overlay(overlay_fdt).map_err(Error::msg).with_context(|| {
                     "Failed to overlay {overlay_file_path:?} onto empty device tree"
@@ -141,7 +141,7 @@
     }
 
     fn as_fdt(&self) -> &Fdt {
-        // SAFETY - Checked validity of buffer when instantiate.
+        // SAFETY: Checked validity of buffer when instantiate.
         unsafe { Fdt::unchecked_from_slice(&self.buffer) }
     }
 }
diff --git a/virtualizationmanager/src/main.rs b/virtualizationmanager/src/main.rs
index bd7f8af..f058547 100644
--- a/virtualizationmanager/src/main.rs
+++ b/virtualizationmanager/src/main.rs
@@ -86,7 +86,7 @@
     }
     owned_fds.push(raw_fd);
 
-    // SAFETY - Initializing OwnedFd for a RawFd provided in cmdline arguments.
+    // SAFETY: Initializing OwnedFd for a RawFd provided in cmdline arguments.
     // We checked that the integer value corresponds to a valid FD and that this
     // is the first argument to claim its ownership.
     Ok(unsafe { OwnedFd::from_raw_fd(raw_fd) })
diff --git a/virtualizationmanager/src/payload.rs b/virtualizationmanager/src/payload.rs
index 733add6..ab6f31c 100644
--- a/virtualizationmanager/src/payload.rs
+++ b/virtualizationmanager/src/payload.rs
@@ -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/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/vm/src/main.rs b/vm/src/main.rs
index 0800f57..0c99acb 100644
--- a/vm/src/main.rs
+++ b/vm/src/main.rs
@@ -82,8 +82,8 @@
         #[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.
@@ -115,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 {
@@ -150,7 +154,7 @@
         #[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,
 
@@ -179,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 {
@@ -299,6 +307,7 @@
             extra_idsigs,
             gdb,
             kernel,
+            vendor,
         } => command_run_app(
             name,
             get_service()?.as_ref(),
@@ -320,6 +329,7 @@
             &extra_idsigs,
             gdb,
             kernel.as_deref(),
+            vendor.as_deref(),
         ),
         Opt::RunMicrodroid {
             name,
@@ -336,6 +346,7 @@
             task_profiles,
             gdb,
             kernel,
+            vendor,
         } => command_run_microdroid(
             name,
             get_service()?.as_ref(),
@@ -352,6 +363,7 @@
             task_profiles,
             gdb,
             kernel.as_deref(),
+            vendor.as_deref(),
         ),
         Opt::Run { name, config, cpu_topology, task_profiles, console, console_in, log, gdb } => {
             command_run(
diff --git a/vm/src/run.rs b/vm/src/run.rs
index 84072ca..f50bd50 100644
--- a/vm/src/run.rs
+++ b/vm/src/run.rs
@@ -65,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")?;
 
@@ -122,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();
 
@@ -144,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 {
@@ -203,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());
@@ -236,6 +241,7 @@
         &extra_sig,
         gdb,
         kernel,
+        vendor,
     )
 }
 
@@ -376,14 +382,14 @@
 /// Safely duplicate the file descriptor.
 fn duplicate_fd<T: AsRawFd>(file: T) -> io::Result<File> {
     let fd = file.as_raw_fd();
-    // Safe because this just duplicates a file descriptor which we know to be valid, and we check
-    // for an error.
+    // SAFETY: This just duplicates a file descriptor which we know to be valid, and we check for an
+    // an error.
     let dup_fd = unsafe { libc::dup(fd) };
     if dup_fd < 0 {
         Err(io::Error::last_os_error())
     } else {
-        // Safe because we have just duplicated the file descriptor so we own it, and `from_raw_fd`
-        // takes ownership of it.
+        // SAFETY: We have just duplicated the file descriptor so we own it, and `from_raw_fd` takes
+        // ownership of it.
         Ok(unsafe { File::from_raw_fd(dup_fd) })
     }
 }
diff --git a/vmbase/Android.bp b/vmbase/Android.bp
index 46f4937..71b9e76 100644
--- a/vmbase/Android.bp
+++ b/vmbase/Android.bp
@@ -84,6 +84,7 @@
         "libspin_nostd",
         "libtinyvec_nostd",
         "libvirtio_drivers",
+        "libzerocopy_nostd",
         "libzeroize_nostd",
     ],
     whole_static_libs: [
diff --git a/vmbase/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 f95958f..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-initialized 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/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..2ce0e83 100644
--- a/vmbase/src/bionic.rs
+++ b/vmbase/src/bionic.rs
@@ -23,12 +23,35 @@
 
 use crate::console;
 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,11 +69,13 @@
 
 #[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 };
 }
 
@@ -58,15 +83,15 @@
 ///
 /// # 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 +125,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 +141,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)) {
diff --git a/vmbase/src/console.rs b/vmbase/src/console.rs
index e9298cc..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) }
 }
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 f67e518..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-initialized writable data.
-pub fn bss_range() -> Range<usize> {
+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..898aa10 100644
--- a/vmbase/src/memory/mod.rs
+++ b/vmbase/src/memory/mod.rs
@@ -22,7 +22,10 @@
 
 pub use error::MemoryTrackerError;
 pub use page_table::PageTable;
-pub use shared::{alloc_shared, dealloc_shared, MemoryRange, MemoryTracker, MEMORY};
+pub use shared::{
+    alloc_shared, dealloc_shared, handle_permission_fault, handle_translation_fault, 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,
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..173c0ec 100644
--- a/vmbase/src/memory/shared.rs
+++ b/vmbase/src/memory/shared.rs
@@ -19,18 +19,20 @@
 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::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 +46,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 +76,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 +89,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 +102,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 +113,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 +139,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 +150,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 +180,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 +227,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 +274,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 +294,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 +317,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 +326,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)
     }
 }
@@ -353,7 +386,7 @@
 /// Unshares all pages when dropped.
 struct MemorySharer {
     granule: usize,
-    shared_regions: Vec<(usize, Layout)>,
+    frames: Vec<(usize, Layout)>,
 }
 
 impl MemorySharer {
@@ -361,42 +394,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 +486,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..48d4c55 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}",
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 c84ca5e..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
@@ -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 534d91a..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();
@@ -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 cfd015a..7c0383b 100644
--- a/vmclient/src/lib.rs
+++ b/vmclient/src/lib.rs
@@ -67,7 +67,7 @@
     // file descriptors (expected by SharedChild).
     let (raw1, raw2) = pipe2(OFlag::O_CLOEXEC)?;
 
-    // SAFETY - Taking ownership of brand new FDs.
+    // SAFETY: Taking ownership of brand new FDs.
     unsafe { Ok((OwnedFd::from_raw_fd(raw1), OwnedFd::from_raw_fd(raw2))) }
 }
 
@@ -80,7 +80,7 @@
     let (raw1, raw2) =
         socketpair(AddressFamily::Unix, SockType::Stream, None, SockFlag::SOCK_CLOEXEC)?;
 
-    // SAFETY - Taking ownership of brand new FDs.
+    // SAFETY: Taking ownership of brand new FDs.
     unsafe { Ok((OwnedFd::from_raw_fd(raw1), OwnedFd::from_raw_fd(raw2))) }
 }
 
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;