Merge "Announce action hint when focused on cursor" into main
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
index cfb1cbe..397a546 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
@@ -21,7 +21,6 @@
 import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
-import android.content.SharedPreferences;
 import android.content.pm.ActivityInfo;
 import android.content.res.Configuration;
 import android.graphics.Bitmap;
@@ -72,6 +71,7 @@
 public class MainActivity extends BaseActivity
         implements VmLauncherService.VmLauncherServiceCallback, AccessibilityStateChangeListener {
     static final String TAG = "VmTerminalApp";
+    static final String KEY_DISK_SIZE = "disk_size";
     private static final String VM_ADDR = "192.168.0.2";
     private static final int TTYD_PORT = 7681;
     private static final int REQUEST_CODE_INSTALLER = 0x33;
@@ -509,17 +509,11 @@
     }
 
     private void resizeDiskIfNecessary(InstalledImage image) {
-        String prefKey = getString(R.string.preference_file_key);
-        String key = getString(R.string.preference_disk_size_key);
-        SharedPreferences sharedPref = this.getSharedPreferences(prefKey, Context.MODE_PRIVATE);
         try {
-            // Use current size as default value to ensure if its size is multiple of 4096
-            long newSize = sharedPref.getLong(key, image.getSize());
-            Log.d(TAG, "Resizing disk to " + newSize + " bytes");
-            newSize = image.resize(newSize);
-            sharedPref.edit().putLong(key, newSize).apply();
+            // TODO(b/382190982): Show snackbar message instead when it's recoverable.
+            image.resize(getIntent().getLongExtra(KEY_DISK_SIZE, image.getSize()));
         } catch (IOException e) {
-            Log.e(TAG, "Failed to resize disk", e);
+            ErrorActivity.start(this, new Exception("Failed to resize disk", e));
             return;
         }
     }
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt
index 30475f5..b893d9e 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt
@@ -15,9 +15,7 @@
  */
 package com.android.virtualization.terminal
 
-import android.content.Context
 import android.content.Intent
-import android.content.SharedPreferences
 import android.icu.text.MeasureFormat
 import android.icu.text.NumberFormat
 import android.icu.util.Measure
@@ -37,12 +35,12 @@
 import java.util.regex.Pattern
 
 class SettingsDiskResizeActivity : AppCompatActivity() {
+    // TODO(b/382191950): Calculate the maxDiskSizeMb based on the device storage usage
     private val maxDiskSizeMb: Long = 16 shl 10
     private val numberPattern: Pattern = Pattern.compile("[\\d]*[\\٫.,]?[\\d]+")
 
     private var diskSizeStepMb: Long = 0
     private var diskSizeMb: Long = 0
-    private lateinit var sharedPref: SharedPreferences
     private lateinit var buttons: View
     private lateinit var cancelButton: View
     private lateinit var resizeButton: View
@@ -71,13 +69,8 @@
 
         diskSizeStepMb = 1L shl resources.getInteger(R.integer.disk_size_round_up_step_size_in_mb)
 
-        sharedPref =
-            this.getSharedPreferences(getString(R.string.preference_file_key), Context.MODE_PRIVATE)
-        diskSizeMb =
-            bytesToMb(
-                sharedPref.getLong(getString(R.string.preference_disk_size_key), /* defValue= */ 0)
-            )
         val image = InstalledImage.getDefault(this)
+        diskSizeMb = bytesToMb(image.getSize())
         val minDiskSizeMb = bytesToMb(image.getSmallestSizePossible()).coerceAtMost(diskSizeMb)
 
         diskSizeText = findViewById<TextView>(R.id.settings_disk_resize_resize_gb_assigned)!!
@@ -139,16 +132,14 @@
             .show()
     }
 
-    fun resize() {
+    private fun resize() {
         diskSizeMb = progressToMb(diskSizeSlider.progress)
         buttons.isVisible = false
-        val editor = sharedPref.edit()
-        editor.putLong(getString(R.string.preference_disk_size_key), mbToBytes(diskSizeMb))
-        editor.apply()
 
         // Restart terminal
         val intent = baseContext.packageManager.getLaunchIntentForPackage(baseContext.packageName)
         intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
+        intent?.putExtra(MainActivity.KEY_DISK_SIZE, mbToBytes(diskSizeMb))
         finish()
         startActivity(intent)
     }
diff --git a/android/TerminalApp/res/values/config.xml b/android/TerminalApp/res/values/config.xml
index 713e1a5..7f0b5e6 100644
--- a/android/TerminalApp/res/values/config.xml
+++ b/android/TerminalApp/res/values/config.xml
@@ -15,8 +15,5 @@
  -->
 
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="preference_file_key" translatable="false">com.android.virtualization.terminal.PREFERENCE_FILE_KEY</string>
-    <string name="preference_disk_size_key" translatable="false">PREFERENCE_DISK_SIZE_KEY</string>
-
     <bool name="terminal_portrait_only">true</bool>
 </resources>
diff --git a/android/virtmgr/src/aidl.rs b/android/virtmgr/src/aidl.rs
index c2f7663..0f81f3d 100644
--- a/android/virtmgr/src/aidl.rs
+++ b/android/virtmgr/src/aidl.rs
@@ -53,6 +53,7 @@
 };
 use android_hardware_security_secretkeeper::aidl::android::hardware::security::secretkeeper::ISecretkeeper::{BnSecretkeeper, ISecretkeeper};
 use android_hardware_security_secretkeeper::aidl::android::hardware::security::secretkeeper::SecretId::SecretId;
+use android_hardware_security_secretkeeper::aidl::android::hardware::security::secretkeeper::PublicKey::PublicKey;
 use android_hardware_security_authgraph::aidl::android::hardware::security::authgraph::{
     Arc::Arc as AuthgraphArc, IAuthGraphKeyExchange::IAuthGraphKeyExchange,
     IAuthGraphKeyExchange::BnAuthGraphKeyExchange, Identity::Identity, KeInitResult::KeInitResult,
@@ -888,16 +889,33 @@
         .context("Failed to extract vendor hashtree digest")
         .or_service_specific_exception(-1)?;
 
-    let trusted_props = if let Some(ref vendor_hashtree_digest) = vendor_hashtree_digest {
+    let vendor_hashtree_digest = if let Some(ref vendor_hashtree_digest) = vendor_hashtree_digest {
         info!(
             "Passing vendor hashtree digest to pvmfw. This will be rejected if it doesn't \
                 match the trusted digest in the pvmfw config, causing the VM to fail to start."
         );
-        vec![(cstr!("vendor_hashtree_descriptor_root_digest"), vendor_hashtree_digest.as_slice())]
+        Some((cstr!("vendor_hashtree_descriptor_root_digest"), vendor_hashtree_digest.as_slice()))
     } else {
-        vec![]
+        None
     };
 
+    let key_material;
+    let secretkeeper_public_key = if is_secretkeeper_supported() {
+        let sk: Strong<dyn ISecretkeeper> = binder::wait_for_interface(SECRETKEEPER_IDENTIFIER)?;
+        if sk.getInterfaceVersion()? >= 2 {
+            let PublicKey { keyMaterial } = sk.getSecretkeeperIdentity()?;
+            key_material = keyMaterial;
+            Some((cstr!("secretkeeper_public_key"), key_material.as_slice()))
+        } else {
+            None
+        }
+    } else {
+        None
+    };
+
+    let trusted_props: Vec<(&CStr, &[u8])> =
+        vec![vendor_hashtree_digest, secretkeeper_public_key].into_iter().flatten().collect();
+
     let instance_id;
     let mut untrusted_props = Vec::with_capacity(2);
     if cfg!(llpvm_changes) {
@@ -2042,6 +2060,14 @@
     fn deleteAll(&self) -> binder::Result<()> {
         self.0.deleteAll()
     }
+
+    fn getSecretkeeperIdentity(&self) -> binder::Result<PublicKey> {
+        // SecretkeeperProxy is really a RPC binder service for PVM (It is called by
+        // MicrodroidManager). PVMs do not & must not (for security reason) rely on
+        // getSecretKeeperIdentity, so we throw an exception if someone attempts to
+        // use this API from the proxy.
+        Err(ExceptionCode::SECURITY.into())
+    }
 }
 
 struct AuthGraphKeyExchangeProxy(Strong<dyn IAuthGraphKeyExchange>);
diff --git a/android/virtualizationservice/aidl/Android.bp b/android/virtualizationservice/aidl/Android.bp
index 79a9d40..db7be71 100644
--- a/android/virtualizationservice/aidl/Android.bp
+++ b/android/virtualizationservice/aidl/Android.bp
@@ -111,7 +111,7 @@
     name: "android.system.virtualmachineservice",
     srcs: ["android/system/virtualmachineservice/**/*.aidl"],
     imports: [
-        "android.hardware.security.secretkeeper-V1",
+        "android.hardware.security.secretkeeper-V2",
         "android.system.virtualizationcommon",
     ],
     unstable: true,
diff --git a/android/virtualizationservice/src/maintenance.rs b/android/virtualizationservice/src/maintenance.rs
index 8e04075..87ba412 100644
--- a/android/virtualizationservice/src/maintenance.rs
+++ b/android/virtualizationservice/src/maintenance.rs
@@ -297,7 +297,9 @@
 mod tests {
     use super::*;
     use android_hardware_security_authgraph::aidl::android::hardware::security::authgraph;
-    use android_hardware_security_secretkeeper::aidl::android::hardware::security::secretkeeper;
+    use android_hardware_security_secretkeeper::aidl::android::hardware::security::secretkeeper::{
+        self, PublicKey::PublicKey,
+    };
     use authgraph::IAuthGraphKeyExchange::IAuthGraphKeyExchange;
     use secretkeeper::ISecretkeeper::BnSecretkeeper;
     use std::sync::{Arc, Mutex};
@@ -335,6 +337,10 @@
             self.history.lock().unwrap().push(SkOp::DeleteAll);
             Ok(())
         }
+
+        fn getSecretkeeperIdentity(&self) -> binder::Result<PublicKey> {
+            unimplemented!()
+        }
     }
     impl binder::Interface for FakeSk {}
 
diff --git a/build/apex/Android.bp b/build/apex/Android.bp
index 6541764..946bc8c 100644
--- a/build/apex/Android.bp
+++ b/build/apex/Android.bp
@@ -108,6 +108,7 @@
                 "rialto_bin",
                 "android_bootloader_crosvm_aarch64",
             ],
+            native_shared_libs: ["libavf"],
         },
         x86_64: {
             binaries: [
@@ -128,6 +129,7 @@
             prebuilts: [
                 "android_bootloader_crosvm_x86_64",
             ],
+            native_shared_libs: ["libavf"],
         },
     },
     binaries: [
diff --git a/build/debian/build.sh b/build/debian/build.sh
index 7231a7c..9104adc 100755
--- a/build/debian/build.sh
+++ b/build/debian/build.sh
@@ -10,15 +10,14 @@
 	echo "Builds a debian image and save it to FILE. [sudo is required]"
 	echo "Options:"
 	echo "-h         Print usage and this help message and exit."
-	echo "-a ARCH    Architecture of the image [default is aarch64]"
+	echo "-a ARCH    Architecture of the image [default is host arch: $(uname -m)]"
 	echo "-r         Release mode build"
-	echo "-w         Save temp work directory (for debugging)"
+	echo "-w         Save temp work directory [for debugging]"
 }
 
 check_sudo() {
 	if [ "$EUID" -ne 0 ]; then
-		echo "Please run as root."
-		exit
+		echo "Please run as root." ; exit 1
 	fi
 }
 
@@ -26,17 +25,10 @@
 	while getopts "a:hrw" option; do
 		case ${option} in
 			h)
-				show_help
-				exit;;
+				show_help ; exit
+				;;
 			a)
-				if [[ "$OPTARG" != "aarch64" && "$OPTARG" != "x86_64" ]]; then
-					echo "Invalid architecture: $OPTARG"
-					exit
-				fi
 				arch="$OPTARG"
-				if [[ "$arch" == "x86_64" ]]; then
-					debian_arch="amd64"
-				fi
 				;;
 			r)
 				mode=release
@@ -45,11 +37,21 @@
 				save_workdir=1
 				;;
 			*)
-				echo "Invalid option: $OPTARG"
-				exit
+				echo "Invalid option: $OPTARG" ; exit 1
 				;;
 		esac
 	done
+	case "$arch" in
+		aarch64)
+			debian_arch="arm64"
+			;;
+		x86_64)
+			debian_arch="amd64"
+			;;
+		*)
+			echo "Invalid architecture: $arch" ; exit 1
+			;;
+	esac
 	if [[ "${*:$OPTIND:1}" ]]; then
 		built_image="${*:$OPTIND:1}"
 	fi
@@ -68,6 +70,7 @@
 install_prerequisites() {
 	apt update
 	packages=(
+		apt-utils
 		automake
 		binfmt-support
 		build-essential
@@ -184,6 +187,7 @@
 	cp -R "${src}"/* "${dst}"
 	cp "$(dirname "$0")/image.yaml" "${resources_dir}"
 
+	cp -R "$(dirname "$0")/localdebs/" "${debian_cloud_image}/"
 	build_ttyd
 	build_rust_binary_and_copy forwarder_guest
 	build_rust_binary_and_copy forwarder_guest_launcher
@@ -217,7 +221,7 @@
 }
 
 clean_up() {
-	[ "$save_workdir" -eq 0 ] || rm -rf "${workdir}"
+	[ "$save_workdir" -eq 1 ] || rm -rf "${workdir}"
 }
 
 set -e
@@ -230,8 +234,7 @@
 debian_version=bookworm
 config_space=${debian_cloud_image}/config_space/${debian_version}
 resources_dir=${debian_cloud_image}/src/debian_cloud_images/resources
-arch=aarch64
-debian_arch=arm64
+arch="$(uname -m)"
 mode=debug
 save_workdir=0
 
diff --git a/build/debian/build_in_container.sh b/build/debian/build_in_container.sh
index 7fd4c00..5028b74 100755
--- a/build/debian/build_in_container.sh
+++ b/build/debian/build_in_container.sh
@@ -1,35 +1,56 @@
 #!/bin/bash
 
-if [ -z "$ANDROID_BUILD_TOP" ]; then echo "forgot to source build/envsetup.sh?" && exit 1; fi
+show_help() {
+  echo "Usage: sudo $0 [OPTION]..."
+  echo "Builds a debian image and save it to image.raw."
+  echo "Options:"
+  echo "-h         Print usage and this help message and exit."
+  echo "-a ARCH    Architecture of the image [default is host arch: $(uname -m)]"
+  echo "-r         Release mode build"
+  echo "-s         Leave a shell open [default: only if the build fails]"
+  echo "-w         Save temp work directory in the container [for debugging]"
+}
 
-arch=aarch64
+arch="$(uname -m)"
 release_flag=
 save_workdir_flag=
+shell_condition="||"
 
-while getopts "a:rw" option; do
+while getopts "a:rsw" option; do
   case ${option} in
     a)
-      if [[ "$OPTARG" != "aarch64" && "$OPTARG" != "x86_64" ]]; then
-        echo "Invalid architecture: $OPTARG"
-        exit
-      fi
       arch="$OPTARG"
       ;;
+    h)
+      show_help ; exit
+      ;;
     r)
       release_flag="-r"
       ;;
+    s)
+      shell_condition=";"
+      ;;
     w)
       save_workdir_flag="-w"
       ;;
     *)
-      echo "Invalid option: $OPTARG"
-      exit
+      echo "Invalid option: $OPTARG" ; exit 1
       ;;
   esac
 done
 
+if [[ "$arch" != "aarch64" && "$arch" != "x86_64" ]]; then
+  echo "Invalid architecture: $arch" ; exit 1
+fi
+
+if [ -z "$ANDROID_BUILD_TOP" ] ; then
+  echo '`ANDROID_BUILD_TOP` is undefined.'
+  echo 'Please `lunch` an Android target, or manually set the variable.'
+  exit 1
+fi
+
 docker run --privileged -it -v /dev:/dev \
   -v "$ANDROID_BUILD_TOP/packages/modules/Virtualization:/root/Virtualization" \
   --workdir /root/Virtualization/build/debian \
   ubuntu:22.04 \
-  bash -c "/root/Virtualization/build/debian/build.sh -a $arch $release_flag $save_workdir_flag || bash"
+  bash -c "/root/Virtualization/build/debian/build.sh -a $arch $release_flag $save_workdir_flag $shell_condition bash"
diff --git a/build/debian/kokoro/gcp_ubuntu_docker/aarch64/build.sh b/build/debian/kokoro/gcp_ubuntu_docker/aarch64/build.sh
index 130e691..43f0338 100644
--- a/build/debian/kokoro/gcp_ubuntu_docker/aarch64/build.sh
+++ b/build/debian/kokoro/gcp_ubuntu_docker/aarch64/build.sh
@@ -5,7 +5,7 @@
 cd "${KOKORO_ARTIFACTS_DIR}/git/avf/build/debian/"
 sudo losetup -D
 grep vmx /proc/cpuinfo || true
-sudo ./build.sh -r
+sudo ./build.sh -r -a aarch64
 sudo mv images.tar.gz ${KOKORO_ARTIFACTS_DIR} || true
 mkdir -p ${KOKORO_ARTIFACTS_DIR}/logs
 sudo cp -r /var/log/fai/* ${KOKORO_ARTIFACTS_DIR}/logs || true
diff --git a/build/debian/kokoro/gcp_ubuntu_docker/x86_64/build.sh b/build/debian/kokoro/gcp_ubuntu_docker/x86_64/build.sh
index 50ded7b..22ac595 100644
--- a/build/debian/kokoro/gcp_ubuntu_docker/x86_64/build.sh
+++ b/build/debian/kokoro/gcp_ubuntu_docker/x86_64/build.sh
@@ -5,8 +5,7 @@
 cd "${KOKORO_ARTIFACTS_DIR}/git/avf/build/debian/"
 sudo losetup -D
 grep vmx /proc/cpuinfo || true
-sudo ./build.sh -a x86_64 -r
+sudo ./build.sh -r -a x86_64
 sudo mv images.tar.gz ${KOKORO_ARTIFACTS_DIR} || true
-
 mkdir -p ${KOKORO_ARTIFACTS_DIR}/logs
 sudo cp -r /var/log/fai/* ${KOKORO_ARTIFACTS_DIR}/logs || true
diff --git a/build/debian/localdebs/.gitkeep b/build/debian/localdebs/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/build/debian/localdebs/.gitkeep
diff --git a/build/microdroid/Android.bp b/build/microdroid/Android.bp
index 7f23ae6..f750f62 100644
--- a/build/microdroid/Android.bp
+++ b/build/microdroid/Android.bp
@@ -50,6 +50,7 @@
     avb_private_key: ":microdroid_sign_key",
     avb_algorithm: "SHA256_RSA4096",
     avb_hash_algorithm: "sha256",
+    use_fec: false,
     partition_name: "system",
     deps: [
         "init_second_stage.microdroid",
@@ -245,6 +246,7 @@
     avb_private_key: ":microdroid_sign_key",
     avb_algorithm: "SHA256_RSA4096",
     avb_hash_algorithm: "sha256",
+    use_fec: false,
     file_contexts: ":microdroid_vendor_file_contexts.gen",
     // For deterministic output, use fake_timestamp, hard-coded uuid
     fake_timestamp: "1611569676",
diff --git a/guest/pvmfw/src/entry.rs b/guest/pvmfw/src/entry.rs
index 48585f3..2f0b391 100644
--- a/guest/pvmfw/src/entry.rs
+++ b/guest/pvmfw/src/entry.rs
@@ -17,21 +17,20 @@
 use crate::config;
 use crate::memory;
 use core::arch::asm;
-use core::mem::{drop, size_of};
+use core::mem::size_of;
 use core::ops::Range;
 use core::slice;
-use hypervisor_backends::get_mmio_guard;
 use log::error;
-use log::info;
 use log::warn;
 use log::LevelFilter;
 use vmbase::util::RangeExt as _;
 use vmbase::{
     arch::aarch64::min_dcache_line_size,
-    configure_heap, console_writeln,
-    layout::{self, crosvm, UART_PAGE_ADDR},
-    main,
-    memory::{MemoryTracker, MEMORY, SIZE_128KB, SIZE_4KB},
+    configure_heap, console_writeln, layout, limit_stack_size, main,
+    memory::{
+        deactivate_dynamic_page_tables, map_image_footer, unshare_all_memory,
+        unshare_all_mmio_except_uart, unshare_uart, MemoryTrackerError, SIZE_128KB, SIZE_4KB,
+    },
     power::reboot,
 };
 use zeroize::Zeroize;
@@ -73,6 +72,7 @@
 
 main!(start);
 configure_heap!(SIZE_128KB);
+limit_stack_size!(SIZE_4KB * 12);
 
 /// Entry point for pVM firmware.
 pub fn start(fdt_address: u64, payload_start: u64, payload_size: u64, _arg3: u64) {
@@ -108,15 +108,11 @@
 
     log::set_max_level(LevelFilter::Info);
 
-    let page_table = memory::init_page_table().map_err(|e| {
-        error!("Failed to set up the dynamic page tables: {e}");
+    let appended_data = get_appended_data_slice().map_err(|e| {
+        error!("Failed to map the appended data: {e}");
         RebootReason::InternalError
     })?;
 
-    // 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() };
-
     let appended = AppendedPayload::new(appended_data).ok_or_else(|| {
         error!("No valid configuration found");
         RebootReason::InvalidConfig
@@ -124,14 +120,6 @@
 
     let config_entries = appended.get_entries();
 
-    // Up to this point, we were using the built-in static (from .rodata) page tables.
-    MEMORY.lock().replace(MemoryTracker::new(
-        page_table,
-        crosvm::MEM_START..layout::MAX_VIRT_ADDR,
-        crosvm::MMIO_RANGE,
-        Some(layout::image_footer_range()),
-    ));
-
     let slices = memory::MemorySlices::new(
         fdt,
         payload,
@@ -152,27 +140,23 @@
     // Writable-dirty regions will be flushed when MemoryTracker is dropped.
     config_entries.bcc.zeroize();
 
-    info!("Expecting a bug making MMIO_GUARD_UNMAP return NOT_SUPPORTED on success");
-    MEMORY.lock().as_mut().unwrap().unshare_all_mmio().map_err(|e| {
+    unshare_all_mmio_except_uart().map_err(|e| {
         error!("Failed to unshare MMIO ranges: {e}");
         RebootReason::InternalError
     })?;
     // Call unshare_all_memory here (instead of relying on the dtor) while UART is still mapped.
-    MEMORY.lock().as_mut().unwrap().unshare_all_memory();
+    unshare_all_memory();
 
-    if let Some(mmio_guard) = get_mmio_guard() {
-        if cfg!(debuggable_vms_improvements) && debuggable_payload {
-            // Keep UART MMIO_GUARD-ed for debuggable payloads, to enable earlycon.
-        } else {
-            mmio_guard.unmap(UART_PAGE_ADDR).map_err(|e| {
-                error!("Failed to unshare the UART: {e}");
-                RebootReason::InternalError
-            })?;
-        }
+    if cfg!(debuggable_vms_improvements) && debuggable_payload {
+        // Keep UART MMIO_GUARD-ed for debuggable payloads, to enable earlycon.
+    } else {
+        unshare_uart().map_err(|e| {
+            error!("Failed to unshare the UART: {e}");
+            RebootReason::InternalError
+        })?;
     }
 
-    // Drop MemoryTracker and deactivate page table.
-    drop(MEMORY.lock().take());
+    deactivate_dynamic_page_tables();
 
     Ok((slices.kernel.as_ptr() as usize, next_bcc))
 }
@@ -199,7 +183,7 @@
     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();
+    let stack = layout::stack_range();
 
     assert_ne!(stack.end - stack.start, 0, "stack region is empty.");
     assert_eq!(stack.start.0 % ASM_STP_ALIGN, 0, "Misaligned stack region.");
@@ -321,15 +305,11 @@
     };
 }
 
-/// # Safety
-///
-/// This must only be called once, since we are returning a mutable reference.
-/// The appended data region must be mapped.
-unsafe fn get_appended_data_slice() -> &'static mut [u8] {
-    let range = layout::image_footer_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.0 as *mut u8, range.end - range.start) }
+fn get_appended_data_slice() -> Result<&'static mut [u8], MemoryTrackerError> {
+    let range = map_image_footer()?;
+    // SAFETY: This region was just mapped for the first time (as map_image_footer() didn't fail)
+    // and the linker script prevents it from overlapping with other objects.
+    Ok(unsafe { slice::from_raw_parts_mut(range.start as *mut u8, range.len()) })
 }
 
 enum AppendedPayload<'a> {
diff --git a/guest/pvmfw/src/helpers.rs b/guest/pvmfw/src/helpers.rs
index 8981408..0552640 100644
--- a/guest/pvmfw/src/helpers.rs
+++ b/guest/pvmfw/src/helpers.rs
@@ -14,7 +14,6 @@
 
 //! Miscellaneous helper functions.
 
-use vmbase::memory::{PAGE_SIZE, SIZE_4KB};
+use vmbase::memory::SIZE_4KB;
 
 pub const GUEST_PAGE_SIZE: usize = SIZE_4KB;
-pub const PVMFW_PAGE_SIZE: usize = PAGE_SIZE;
diff --git a/guest/pvmfw/src/main.rs b/guest/pvmfw/src/main.rs
index 612281b..bde03ff 100644
--- a/guest/pvmfw/src/main.rs
+++ b/guest/pvmfw/src/main.rs
@@ -55,7 +55,6 @@
 use vmbase::fdt::pci::{PciError, PciInfo};
 use vmbase::heap;
 use vmbase::memory::flush;
-use vmbase::memory::MEMORY;
 use vmbase::rand;
 use vmbase::virtio::pci;
 
@@ -101,7 +100,7 @@
     // Set up PCI bus for VirtIO devices.
     let pci_info = PciInfo::from_fdt(fdt).map_err(handle_pci_error)?;
     debug!("PCI: {:#x?}", pci_info);
-    let mut pci_root = pci::initialize(pci_info, MEMORY.lock().as_mut().unwrap()).map_err(|e| {
+    let mut pci_root = pci::initialize(pci_info).map_err(|e| {
         error!("Failed to initialize PCI: {e}");
         RebootReason::InternalError
     })?;
diff --git a/guest/pvmfw/src/memory.rs b/guest/pvmfw/src/memory.rs
index 7d49bca..35bfd3a 100644
--- a/guest/pvmfw/src/memory.rs
+++ b/guest/pvmfw/src/memory.rs
@@ -16,48 +16,17 @@
 
 use crate::entry::RebootReason;
 use crate::fdt;
-use crate::helpers::PVMFW_PAGE_SIZE;
-use aarch64_paging::paging::VirtualAddress;
-use aarch64_paging::MapError;
 use core::num::NonZeroUsize;
-use core::ops::Range;
-use core::result;
 use core::slice;
-use hypervisor_backends::get_mem_sharer;
 use log::debug;
 use log::error;
 use log::info;
 use log::warn;
 use vmbase::{
-    layout::{self, crosvm},
-    memory::{PageTable, MEMORY},
+    layout::crosvm,
+    memory::{init_shared_pool, map_data, map_rodata, resize_available_memory},
 };
 
-/// Region allocated for the stack.
-pub fn stack_range() -> Range<VirtualAddress> {
-    const STACK_PAGES: usize = 12;
-
-    layout::stack_range(STACK_PAGES * PVMFW_PAGE_SIZE)
-}
-
-pub fn init_page_table() -> result::Result<PageTable, MapError> {
-    let mut page_table = PageTable::default();
-
-    // 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::data_bss_range().into())?;
-    page_table.map_data(&layout::eh_stack_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(&layout::image_footer_range().into())?;
-    if let Err(e) = page_table.map_device(&layout::console_uart_page().into()) {
-        error!("Failed to remap the UART as a dynamic page table entry: {e}");
-        return Err(e);
-    }
-    Ok(page_table)
-}
-
 pub(crate) struct MemorySlices<'a> {
     pub fdt: &'a mut libfdt::Fdt,
     pub kernel: &'a [u8],
@@ -76,13 +45,13 @@
         // 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| {
+        map_data(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.
-        let fdt = unsafe { slice::from_raw_parts_mut(range.start as *mut u8, range.len()) };
+        // SAFETY: map_data validated the range to be in main memory, mapped, and not overlap.
+        let fdt = unsafe { slice::from_raw_parts_mut(fdt as *mut u8, fdt_size.into()) };
 
         let info = fdt::sanitize_device_tree(fdt, vm_dtbo, vm_ref_dt)?;
         let fdt = libfdt::Fdt::from_mut_slice(fdt).map_err(|e| {
@@ -93,67 +62,56 @@
 
         let memory_range = info.memory_range;
         debug!("Resizing MemoryTracker to range {memory_range:#x?}");
-        MEMORY.lock().as_mut().unwrap().shrink(&memory_range).map_err(|e| {
+        resize_available_memory(&memory_range).map_err(|e| {
             error!("Failed to use memory range value from DT: {memory_range:#x?}: {e}");
             RebootReason::InvalidFdt
         })?;
 
-        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
-            })?;
-            MEMORY.lock().as_mut().unwrap().init_dynamic_shared_pool(granule).map_err(|e| {
-                error!("Failed to initialize dynamically shared pool: {e}");
-                RebootReason::InternalError
-            })?;
-        } else {
-            let range = info.swiotlb_info.fixed_range().ok_or_else(|| {
-                error!("Pre-shared pool range not specified in swiotlb node");
-                RebootReason::InvalidFdt
-            })?;
+        init_shared_pool(info.swiotlb_info.fixed_range()).map_err(|e| {
+            error!("Failed to initialize shared pool: {e}");
+            RebootReason::InternalError
+        })?;
 
-            MEMORY.lock().as_mut().unwrap().init_static_shared_pool(range).map_err(|e| {
-                error!("Failed to initialize pre-shared pool {e}");
-                RebootReason::InvalidFdt
-            })?;
-        }
-
-        let kernel_range = if let Some(r) = info.kernel_range {
-            MEMORY.lock().as_mut().unwrap().alloc_range(&r).map_err(|e| {
-                error!("Failed to obtain the kernel range with DT range: {e}");
+        let (kernel_start, kernel_size) = if let Some(r) = info.kernel_range {
+            let size = r.len().try_into().map_err(|_| {
+                error!("Invalid kernel size: {:#x}", r.len());
                 RebootReason::InternalError
-            })?
+            })?;
+            (r.start, size)
         } else if cfg!(feature = "legacy") {
             warn!("Failed to find the kernel range in the DT; falling back to legacy ABI");
-
-            let kernel_size = NonZeroUsize::new(kernel_size).ok_or_else(|| {
+            let size = NonZeroUsize::new(kernel_size).ok_or_else(|| {
                 error!("Invalid kernel size: {kernel_size:#x}");
                 RebootReason::InvalidPayload
             })?;
-
-            MEMORY.lock().as_mut().unwrap().alloc(kernel, kernel_size).map_err(|e| {
-                error!("Failed to obtain the kernel range with legacy range: {e}");
-                RebootReason::InternalError
-            })?
+            (kernel, size)
         } else {
             error!("Failed to locate the kernel from the DT");
             return Err(RebootReason::InvalidPayload);
         };
 
-        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()) };
+        map_rodata(kernel_start, kernel_size).map_err(|e| {
+            error!("Failed to map kernel range: {e}");
+            RebootReason::InternalError
+        })?;
+
+        let kernel = kernel_start as *const u8;
+        // SAFETY: map_rodata validated the range to be in main memory, mapped, and not overlap.
+        let kernel = unsafe { slice::from_raw_parts(kernel, kernel_size.into()) };
 
         let ramdisk = if let Some(r) = info.initrd_range {
             debug!("Located ramdisk at {r:?}");
-            let r = MEMORY.lock().as_mut().unwrap().alloc_range(&r).map_err(|e| {
+            let ramdisk_size = r.len().try_into().map_err(|_| {
+                error!("Invalid ramdisk size: {:#x}", r.len());
+                RebootReason::InvalidRamdisk
+            })?;
+            map_rodata(r.start, ramdisk_size).map_err(|e| {
                 error!("Failed to obtain the initrd range: {e}");
                 RebootReason::InvalidRamdisk
             })?;
 
-            // SAFETY: The region was validated by memory to be in main memory, mapped, and
-            // not overlap.
+            // SAFETY: map_rodata validated the range to be in main memory, mapped, and not
+            // overlap.
             Some(unsafe { slice::from_raw_parts(r.start as *const u8, r.len()) })
         } else {
             info!("Couldn't locate the ramdisk from the device tree");
diff --git a/guest/rialto/src/main.rs b/guest/rialto/src/main.rs
index 61e9948..04d18be 100644
--- a/guest/rialto/src/main.rs
+++ b/guest/rialto/src/main.rs
@@ -32,8 +32,6 @@
 use core::num::NonZeroUsize;
 use core::slice;
 use diced_open_dice::{bcc_handover_parse, DiceArtifacts};
-use hypervisor_backends::get_mem_sharer;
-use libfdt::FdtError;
 use log::{debug, error, info};
 use service_vm_comm::{ServiceVmRequest, VmType};
 use service_vm_fake_chain::service_vm;
@@ -48,9 +46,12 @@
     fdt::pci::PciInfo,
     fdt::SwiotlbInfo,
     generate_image_header,
-    layout::{self, crosvm},
+    layout::crosvm,
     main,
-    memory::{MemoryTracker, PageTable, MEMORY, PAGE_SIZE, SIZE_128KB},
+    memory::{
+        init_shared_pool, map_rodata, map_rodata_outside_main_memory, resize_available_memory,
+        SIZE_128KB,
+    },
     power::reboot,
     virtio::{
         pci::{self, PciTransportIterator, VirtIOSocket},
@@ -70,82 +71,45 @@
     }
 }
 
-fn new_page_table() -> Result<PageTable> {
-    let mut page_table = PageTable::default();
-
-    page_table.map_data(&layout::data_bss_range().into())?;
-    page_table.map_data(&layout::eh_stack_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_page().into())?;
-
-    Ok(page_table)
-}
-
 /// # Safety
 ///
 /// Behavior is undefined if any of the following conditions are violated:
 /// * The `fdt_addr` must be a valid pointer and points to a valid `Fdt`.
 unsafe fn try_main(fdt_addr: usize) -> Result<()> {
     info!("Welcome to Rialto!");
-    let page_table = new_page_table()?;
 
-    MEMORY.lock().replace(MemoryTracker::new(
-        page_table,
-        crosvm::MEM_START..layout::MAX_VIRT_ADDR,
-        crosvm::MMIO_RANGE,
-        None, // Rialto doesn't have any payload for now.
-    ));
-
-    let fdt_range = MEMORY
-        .lock()
-        .as_mut()
-        .unwrap()
-        .alloc(fdt_addr, NonZeroUsize::new(crosvm::FDT_MAX_SIZE).unwrap())?;
+    let fdt_size = NonZeroUsize::new(crosvm::FDT_MAX_SIZE).unwrap();
+    map_rodata(fdt_addr, fdt_size)?;
     // SAFETY: The tracker validated the range to be in main memory, mapped, and not overlap.
-    let fdt = unsafe { slice::from_raw_parts(fdt_range.start as *mut u8, fdt_range.len()) };
+    let fdt = unsafe { slice::from_raw_parts(fdt_addr as *mut u8, fdt_size.into()) };
     // We do not need to validate the DT since it is already validated in pvmfw.
     let fdt = libfdt::Fdt::from_slice(fdt)?;
 
     let memory_range = fdt.first_memory_range()?;
-    MEMORY.lock().as_mut().unwrap().shrink(&memory_range).inspect_err(|_| {
+    resize_available_memory(&memory_range).inspect_err(|_| {
         error!("Failed to use memory range value from DT: {memory_range:#x?}");
     })?;
 
-    if let Some(mem_sharer) = get_mem_sharer() {
-        let granule = mem_sharer.granule()?;
-        MEMORY.lock().as_mut().unwrap().init_dynamic_shared_pool(granule).inspect_err(|_| {
-            error!("Failed to initialize dynamically shared pool.");
-        })?;
-    } else if let Ok(Some(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)
-        })?;
-        MEMORY.lock().as_mut().unwrap().init_static_shared_pool(range).inspect_err(|_| {
-            error!("Failed to initialize pre-shared pool.");
-        })?;
-    } else {
-        info!("No MEM_SHARE capability detected or swiotlb found: allocating buffers from heap.");
-        MEMORY.lock().as_mut().unwrap().init_heap_shared_pool().inspect_err(|_| {
-            error!("Failed to initialize heap-based pseudo-shared pool.");
-        })?;
-    }
+    let swiotlb_range = SwiotlbInfo::new_from_fdt(fdt)
+        .inspect_err(|_| {
+            error!("Rialto failed when access swiotlb");
+        })?
+        .and_then(|info| info.fixed_range());
+    init_shared_pool(swiotlb_range).inspect_err(|_| {
+        error!("Failed to initialize shared pool.");
+    })?;
 
     let bcc_handover: Box<dyn DiceArtifacts> = match vm_type(fdt)? {
         VmType::ProtectedVm => {
             let dice_range = read_dice_range_from(fdt)?;
             info!("DICE range: {dice_range:#x?}");
-            // SAFETY: This region was written by pvmfw in its writable_data region. The region
-            // has no overlap with the main memory region and is safe to be mapped as read-only
-            // data.
-            let res = unsafe {
-                MEMORY.lock().as_mut().unwrap().alloc_range_outside_main_memory(&dice_range)
-            };
-            res.inspect_err(|_| {
-                error!("Failed to use DICE range from DT: {dice_range:#x?}");
-            })?;
+            let dice_size = dice_range.len().try_into().unwrap();
+            // SAFETY: The DICE memory region has been generated by pvmfw and doesn't overlap.
+            unsafe { map_rodata_outside_main_memory(dice_range.start, dice_size) }.inspect_err(
+                |_| {
+                    error!("Failed to use DICE range from DT: {dice_range:#x?}");
+                },
+            )?;
             let dice_start = dice_range.start as *const u8;
             // SAFETY: There's no memory overlap and the region is mapped as read-only data.
             let bcc_handover = unsafe { slice::from_raw_parts(dice_start, dice_range.len()) };
@@ -158,8 +122,7 @@
 
     let pci_info = PciInfo::from_fdt(fdt)?;
     debug!("PCI: {pci_info:#x?}");
-    let mut pci_root = pci::initialize(pci_info, MEMORY.lock().as_mut().unwrap())
-        .map_err(Error::PciInitializationFailed)?;
+    let mut pci_root = pci::initialize(pci_info).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());
diff --git a/guest/trusty/security_vm/TEST_MAPPING b/guest/trusty/security_vm/TEST_MAPPING
new file mode 100644
index 0000000..ad7b899
--- /dev/null
+++ b/guest/trusty/security_vm/TEST_MAPPING
@@ -0,0 +1,15 @@
+{
+  "trusty-security_vm-presubmit": [
+  ],
+  "trusty-security_vm-postsubmit": [
+    {
+      "name": "VtsAidlKeyMintTargetTest"
+    },
+    {
+      "name": "VtsAidlSharedSecretTargetTest"
+    },
+    {
+      "name": "VtsHalRemotelyProvisionedComponentTargetTest"
+    }
+  ]
+}
diff --git a/guest/vmbase_example/src/layout.rs b/guest/vmbase_example/src/layout.rs
index 4e87e4e..bafce10 100644
--- a/guest/vmbase_example/src/layout.rs
+++ b/guest/vmbase_example/src/layout.rs
@@ -14,15 +14,8 @@
 
 //! Memory layout.
 
-use aarch64_paging::paging::VirtualAddress;
-use core::ops::Range;
 use log::info;
-use vmbase::{layout, memory::PAGE_SIZE};
-
-/// Writable data region for the stack.
-pub fn boot_stack_range() -> Range<VirtualAddress> {
-    layout::stack_range(40 * PAGE_SIZE)
-}
+use vmbase::layout;
 
 pub fn print_addresses() {
     let text = layout::text_range();
@@ -40,7 +33,7 @@
     );
     let bss = layout::bss_range();
     info!("bss:        {}..{} ({} bytes)", bss.start, bss.end, bss.end - bss.start);
-    let boot_stack = boot_stack_range();
+    let boot_stack = layout::stack_range();
     info!(
         "boot_stack: {}..{} ({} bytes)",
         boot_stack.start,
diff --git a/guest/vmbase_example/src/main.rs b/guest/vmbase_example/src/main.rs
index f00effa..4c5e880 100644
--- a/guest/vmbase_example/src/main.rs
+++ b/guest/vmbase_example/src/main.rs
@@ -23,12 +23,9 @@
 
 extern crate alloc;
 
-use crate::layout::{boot_stack_range, print_addresses};
-use crate::pci::{check_pci, get_bar_region, get_cam_region};
-use aarch64_paging::paging::VirtualAddress;
-use aarch64_paging::MapError;
+use crate::layout::print_addresses;
+use crate::pci::check_pci;
 use alloc::{vec, vec::Vec};
-use core::mem;
 use core::ptr::addr_of_mut;
 use cstr::cstr;
 use libfdt::Fdt;
@@ -37,12 +34,9 @@
     bionic, configure_heap,
     fdt::pci::PciInfo,
     generate_image_header,
-    layout::{
-        console_uart_page, crosvm::FDT_MAX_SIZE, data_bss_range, eh_stack_range, rodata_range,
-        text_range,
-    },
+    layout::crosvm::FDT_MAX_SIZE,
     linker, logger, main,
-    memory::{PageTable, SIZE_64KB},
+    memory::{deactivate_dynamic_page_tables, map_data, SIZE_64KB},
 };
 
 static INITIALISED_DATA: [u32; 4] = [1, 2, 3, 4];
@@ -53,25 +47,6 @@
 main!(main);
 configure_heap!(SIZE_64KB);
 
-fn init_page_table(page_table: &mut PageTable) -> Result<(), MapError> {
-    page_table.map_device(&console_uart_page().into())?;
-    page_table.map_code(&text_range().into())?;
-    page_table.map_rodata(&rodata_range().into())?;
-    page_table.map_data(&data_bss_range().into())?;
-    page_table.map_data(&eh_stack_range().into())?;
-    page_table.map_data(&boot_stack_range().into())?;
-
-    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) {
     log::set_max_level(LevelFilter::Debug);
@@ -82,15 +57,11 @@
     check_data();
     check_stack_guard();
 
-    let mut page_table = PageTable::default();
-    init_page_table(&mut page_table).unwrap();
-
     info!("Checking FDT...");
     let fdt_addr = usize::try_from(arg0).unwrap();
     // 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_addr as *mut u8, FDT_MAX_SIZE) };
-    let fdt_region = (VirtualAddress(fdt_addr)..VirtualAddress(fdt_addr + fdt.len())).into();
-    page_table.map_data(&fdt_region).unwrap();
+    map_data(fdt_addr, FDT_MAX_SIZE.try_into().unwrap()).unwrap();
     let fdt = Fdt::from_mut_slice(fdt).unwrap();
     info!("FDT passed verification.");
     check_fdt(fdt);
@@ -101,23 +72,16 @@
     modify_fdt(fdt);
 
     check_alloc();
-
-    let cam_region = get_cam_region(&pci_info);
-    page_table.map_device(&cam_region).unwrap();
-    let bar_region = get_bar_region(&pci_info);
-    page_table.map_device(&bar_region).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() };
+    let mut pci_root = vmbase::virtio::pci::initialize(pci_info).unwrap();
     check_pci(&mut pci_root);
 
     emit_suppressed_log();
 
     info!("De-activating IdMap...");
-    mem::drop(page_table); // Release PageTable and switch back to idmap.S
+    deactivate_dynamic_page_tables();
     info!("De-activated.");
 }
 
diff --git a/guest/vmbase_example/src/pci.rs b/guest/vmbase_example/src/pci.rs
index 379425d..32ab9f6 100644
--- a/guest/vmbase_example/src/pci.rs
+++ b/guest/vmbase_example/src/pci.rs
@@ -14,7 +14,6 @@
 
 //! Functions to scan the PCI bus for VirtIO device.
 
-use aarch64_paging::paging::MemoryRegion;
 use alloc::alloc::{alloc_zeroed, dealloc, handle_alloc_error, Layout};
 use core::{mem::size_of, ptr::NonNull};
 use log::{debug, info};
@@ -26,10 +25,7 @@
     },
     BufferDirection, Error, Hal, PhysAddr, PAGE_SIZE,
 };
-use vmbase::{
-    fdt::pci::PciInfo,
-    virtio::pci::{self, PciTransportIterator},
-};
+use vmbase::virtio::pci::{self, PciTransportIterator};
 
 /// The standard sector size of a VirtIO block device, in bytes.
 const SECTOR_SIZE_BYTES: usize = 512;
@@ -115,16 +111,6 @@
     info!("Wrote to VirtIO console.");
 }
 
-/// Gets the memory region in which BARs are allocated.
-pub fn get_bar_region(pci_info: &PciInfo) -> MemoryRegion {
-    MemoryRegion::new(pci_info.bar_range.start as usize, pci_info.bar_range.end as usize)
-}
-
-/// Gets the PCI CAM memory region.
-pub fn get_cam_region(pci_info: &PciInfo) -> MemoryRegion {
-    MemoryRegion::new(pci_info.cam_range.start, pci_info.cam_range.end)
-}
-
 struct HalImpl;
 
 /// SAFETY: See the 'Implementation Safety' comments on methods below for how they fulfill the
diff --git a/libs/dice/open_dice/Android.bp b/libs/dice/open_dice/Android.bp
index 4241c47..f799fb1 100644
--- a/libs/dice/open_dice/Android.bp
+++ b/libs/dice/open_dice/Android.bp
@@ -156,7 +156,7 @@
         "--allowlist-var=DICE_INLINE_CONFIG_SIZE",
         "--allowlist-var=DICE_PRIVATE_KEY_SEED_SIZE",
         "--allowlist-var=DICE_ID_SIZE",
-        "--allowlist-var=DICE_PRIVATE_KEY_SIZE",
+        "--allowlist-var=DICE_PRIVATE_KEY_BUFFER_SIZE",
     ],
 }
 
diff --git a/libs/dice/open_dice/src/dice.rs b/libs/dice/open_dice/src/dice.rs
index 206769c..6c7d48d 100644
--- a/libs/dice/open_dice/src/dice.rs
+++ b/libs/dice/open_dice/src/dice.rs
@@ -21,7 +21,7 @@
 use open_dice_cbor_bindgen::{
     DiceConfigType, DiceDeriveCdiCertificateId, DiceDeriveCdiPrivateKeySeed, DiceInputValues,
     DiceMainFlow, DICE_CDI_SIZE, DICE_HASH_SIZE, DICE_HIDDEN_SIZE, DICE_ID_SIZE,
-    DICE_INLINE_CONFIG_SIZE, DICE_PRIVATE_KEY_SEED_SIZE, DICE_PRIVATE_KEY_SIZE,
+    DICE_INLINE_CONFIG_SIZE, DICE_PRIVATE_KEY_BUFFER_SIZE, DICE_PRIVATE_KEY_SEED_SIZE,
 };
 #[cfg(feature = "multialg")]
 use open_dice_cbor_bindgen::{DiceContext_, DiceKeyAlgorithm};
@@ -41,7 +41,7 @@
 /// The size of a private key seed.
 pub const PRIVATE_KEY_SEED_SIZE: usize = DICE_PRIVATE_KEY_SEED_SIZE as usize;
 /// The size of a private key.
-pub const PRIVATE_KEY_SIZE: usize = DICE_PRIVATE_KEY_SIZE as usize;
+pub const PRIVATE_KEY_SIZE: usize = DICE_PRIVATE_KEY_BUFFER_SIZE as usize;
 /// The size of an ID.
 pub const ID_SIZE: usize = DICE_ID_SIZE as usize;
 
diff --git a/libs/libavf/Android.bp b/libs/libavf/Android.bp
new file mode 100644
index 0000000..e143709
--- /dev/null
+++ b/libs/libavf/Android.bp
@@ -0,0 +1,58 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_bindgen {
+    name: "libavf_bindgen",
+    wrapper_src: "include/android/virtualization.h",
+    crate_name: "avf_bindgen",
+    defaults: ["avf_build_flags_rust"],
+    source_stem: "bindings",
+    bindgen_flags: ["--default-enum-style rust"],
+    apex_available: ["com.android.virt"],
+}
+
+rust_defaults {
+    name: "libavf.default",
+    crate_name: "avf",
+    defaults: ["avf_build_flags_rust"],
+    srcs: ["src/lib.rs"],
+    edition: "2021",
+    rustlibs: [
+        "libvmclient",
+        "android.system.virtualizationcommon-rust",
+        "android.system.virtualizationservice-rust",
+        "libavf_bindgen",
+        "libbinder_rs",
+        "liblibc",
+        "liblog_rust",
+        "librpcbinder_rs",
+    ],
+    apex_available: ["com.android.virt"],
+}
+
+rust_ffi_static {
+    name: "libavf_impl",
+    defaults: ["libavf.default"],
+    export_include_dirs: ["include"],
+}
+
+cc_library {
+    name: "libavf",
+    llndk: {
+        symbol_file: "libavf.map.txt",
+        moved_to_apex: true,
+    },
+    whole_static_libs: ["libavf_impl"],
+    shared_libs: [
+        "libbinder_ndk",
+        "libbinder_rpc_unstable",
+        "liblog",
+    ],
+    export_static_lib_headers: ["libavf_impl"],
+    apex_available: ["com.android.virt"],
+    version_script: "libavf.map.txt",
+    stubs: {
+        symbol_file: "libavf.map.txt",
+    },
+}
diff --git a/libs/libavf/include/android/virtualization.h b/libs/libavf/include/android/virtualization.h
new file mode 100644
index 0000000..f33ee75
--- /dev/null
+++ b/libs/libavf/include/android/virtualization.h
@@ -0,0 +1,326 @@
+/*
+ * Copyright 2024 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.
+ */
+#pragma once
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+__BEGIN_DECLS
+
+/**
+ * Represents a handle on a virtual machine raw config.
+ */
+typedef struct AVirtualMachineRawConfig AVirtualMachineRawConfig;
+
+/**
+ * Create a new virtual machine raw config object with no properties.
+ *
+ * This only creates the raw config object. `name` and `kernel` must be set with
+ * calls to {@link AVirtualMachineRawConfig_setName} and {@link AVirtualMachineRawConfig_setKernel}.
+ * Other properties, set by {@link AVirtualMachineRawConfig_setMemoryMib},
+ * {@link AVirtualMachineRawConfig_setInitRd}, {@link AVirtualMachineRawConfig_addDisk},
+ * {@link AVirtualMachineRawConfig_setProtectedVm}, and {@link AVirtualMachineRawConfig_setBalloon}
+ * are optional.
+ *
+ * The caller takes ownership of the returned raw config object, and is responsible for creating a
+ * VM by calling {@link AVirtualMachine_createRaw} or releasing it by calling
+ * {@link AVirtualMachineRawConfig_destroy}.
+ *
+ * \return A new virtual machine raw config object.
+ */
+AVirtualMachineRawConfig* AVirtualMachineRawConfig_create();
+
+/**
+ * Destroy a virtual machine config object.
+ *
+ * \param config a virtual machine config object.
+ *
+ * `AVirtualMachineRawConfig_destroy` does nothing if `config` is null. A destroyed config object
+ * must not be reused.
+ */
+void AVirtualMachineRawConfig_destroy(AVirtualMachineRawConfig* config);
+
+/**
+ * Set a name of a virtual machine.
+ *
+ * \param config a virtual machine config object.
+ * \param name a pointer to a null-terminated string for the name.
+ *
+ * \return If successful, it returns 0.
+ */
+int AVirtualMachineRawConfig_setName(AVirtualMachineRawConfig* config, const char* name);
+
+/**
+ * Set an instance ID of a virtual machine.
+ *
+ * \param config a virtual machine config object.
+ * \param instanceId a pointer to a 64-byte buffer for the instance ID.
+ *
+ * \return If successful, it returns 0.
+ */
+int AVirtualMachineRawConfig_setInstanceId(AVirtualMachineRawConfig* config,
+                                           const int8_t* instanceId);
+
+/**
+ * Set a kernel image of a virtual machine.
+ *
+ * \param config a virtual machine config object.
+ * \param fd a readable file descriptor containing the kernel image, or -1 to unset.
+ *   `AVirtualMachineRawConfig_setKernel` takes ownership of `fd`.
+ *
+ * \return If successful, it returns 0.
+ */
+int AVirtualMachineRawConfig_setKernel(AVirtualMachineRawConfig* config, int fd);
+
+/**
+ * Set an init rd of a virtual machine.
+ *
+ * \param config a virtual machine config object.
+ * \param fd a readable file descriptor containing the kernel image, or -1 to unset.
+ *   `AVirtualMachineRawConfig_setInitRd` takes ownership of `fd`.
+ *
+ * \return If successful, it returns 0.
+ */
+int AVirtualMachineRawConfig_setInitRd(AVirtualMachineRawConfig* config, int fd);
+
+/**
+ * Add a disk for a virtual machine.
+ *
+ * \param config a virtual machine config object.
+ * \param fd a readable file descriptor containing the disk image.
+ * `AVirtualMachineRawConfig_addDisk` takes ownership of `fd`.
+ *
+ * \return If successful, it returns 0. If `fd` is invalid, it returns -EINVAL.
+ */
+int AVirtualMachineRawConfig_addDisk(AVirtualMachineRawConfig* config, int fd);
+
+/**
+ * Set how much memory will be given to a virtual machine.
+ *
+ * \param config a virtual machine config object.
+ * \param memoryMib the amount of RAM to give the virtual machine, in MiB. 0 or negative to use the
+ *   default.
+ *
+ * \return If successful, it returns 0.
+ */
+int AVirtualMachineRawConfig_setMemoryMib(AVirtualMachineRawConfig* config, int32_t memoryMib);
+
+/**
+ * Set whether a virtual machine is protected or not.
+ *
+ * \param config a virtual machine config object.
+ * \param protectedVm whether the virtual machine should be protected.
+ *
+ * \return If successful, it returns 0.
+ */
+int AVirtualMachineRawConfig_setProtectedVm(AVirtualMachineRawConfig* config, bool protectedVm);
+
+/**
+ * Set whether a virtual machine uses memory ballooning or not.
+ *
+ * \param config a virtual machine config object.
+ * \param balloon whether the virtual machine should use memory ballooning.
+ *
+ * \return If successful, it returns 0.
+ */
+int AVirtualMachineRawConfig_setBalloon(AVirtualMachineRawConfig* config, bool balloon);
+
+/**
+ * Set whether to use an alternate, hypervisor-specific authentication method
+ * for protected VMs. You don't want to use this.
+ *
+ * \return If successful, it returns 0. It returns `-ENOTSUP` if the hypervisor doesn't have an
+ *   alternate auth mode.
+ */
+int AVirtualMachineRawConfig_setHypervisorSpecificAuthMethod(AVirtualMachineRawConfig* config,
+                                                             bool enable);
+
+/**
+ * Use the specified fd as the backing memfd for a range of the guest
+ * physical memory.
+ *
+ * \param config a virtual machine config object.
+ * \param fd a memfd
+ * \param rangeStart range start IPA
+ * \param rangeEnd range end IPA
+ *
+ * \return If successful, it returns 0. It returns `-ENOTSUP` if the hypervisor doesn't support
+ *   backing memfd.
+ */
+int AVirtualMachineRawConfig_addCustomMemoryBackingFile(AVirtualMachineRawConfig* config, int fd,
+                                                        size_t rangeStart, size_t rangeEnd);
+
+/**
+ * Represents a handle on a virtualization service, responsible for managing virtual machines.
+ */
+typedef struct AVirtualizationService AVirtualizationService;
+
+/**
+ * Spawn a new instance of `virtmgr`, a child process that will host the `VirtualizationService`
+ * service, and connect to the child process.
+ *
+ * The caller takes ownership of the returned service object, and is responsible for releasing it
+ * by calling {@link AVirtualizationService_destroy}.
+ *
+ * \param early set to true when running a service for early virtual machines. See
+ *   [`early_vm.md`](../../../../docs/early_vm.md) for more details on early virtual machines.
+ * \param service an out parameter that will be set to the service handle.
+ *
+ * \return
+ *   - If successful, it sets `service` and returns 0.
+ *   - If it fails to spawn `virtmgr`, it leaves `service` untouched and returns a negative value
+ *     representing the OS error code.
+ *   - If it fails to connect to the spawned `virtmgr`, it leaves `service` untouched and returns
+ *     `-ECONNREFUSED`.
+ */
+int AVirtualizationService_create(AVirtualizationService** service, bool early);
+
+/**
+ * Destroy a VirtualizationService object.
+ *
+ * `AVirtualizationService_destroy` does nothing if `service` is null. A destroyed service object
+ * must not be reused.
+ *
+ * \param service a handle on a virtualization service.
+ */
+void AVirtualizationService_destroy(AVirtualizationService* service);
+
+/**
+ * Represents a handle on a virtual machine.
+ */
+typedef struct AVirtualMachine AVirtualMachine;
+
+/**
+ * The reason why a virtual machine stopped.
+ * @see AVirtualMachine_waitForStop
+ */
+enum StopReason : int32_t {
+    /**
+     * VirtualizationService died.
+     */
+    VIRTUALIZATION_SERVICE_DIED = 1,
+    /**
+     * There was an error waiting for the virtual machine.
+     */
+    INFRASTRUCTURE_ERROR = 2,
+    /**
+     * The virtual machine was killed.
+     */
+    KILLED = 3,
+    /**
+     * The virtual machine stopped for an unknown reason.
+     */
+    UNKNOWN = 4,
+    /**
+     * The virtual machine requested to shut down.
+     */
+    SHUTDOWN = 5,
+    /**
+     * crosvm had an error starting the virtual machine.
+     */
+    START_FAILED = 6,
+    /**
+     * The virtual machine requested to reboot, possibly as the result of a kernel panic.
+     */
+    REBOOT = 7,
+    /**
+     * The virtual machine or crosvm crashed.
+     */
+    CRASH = 8,
+    /**
+     * The pVM firmware failed to verify the VM because the public key doesn't match.
+     */
+    PVM_FIRMWARE_PUBLIC_KEY_MISMATCH = 9,
+    /**
+     * The pVM firmware failed to verify the VM because the instance image changed.
+     */
+    PVM_FIRMWARE_INSTANCE_IMAGE_CHANGED = 10,
+    /**
+     * The virtual machine was killed due to hangup.
+     */
+    HANGUP = 11,
+    /**
+     * VirtualizationService sent a stop reason which was not recognised by the client library.
+     */
+    UNRECOGNISED = 0,
+};
+
+/**
+ * Create a virtual machine with given raw `config`.
+ *
+ * The created virtual machine is in stopped state. To run the created virtual machine, call
+ * {@link AVirtualMachine_start}.
+ *
+ * The caller takes ownership of the returned virtual machine object, and is responsible for
+ * releasing it by calling {@link AVirtualMachine_destroy}.
+ *
+ * \param service a handle on a virtualization service.
+ * \param config a virtual machine config object. Ownership will always be transferred from the
+ *   caller, even if unsuccessful. `config` must not be reused.
+ * \param consoleOutFd a writable file descriptor for the console output, or -1. Ownership will
+ *   always be transferred from the caller, even if unsuccessful.
+ * \param consoleInFd a readable file descriptor for the console input, or -1. Ownership will always
+ *   be transferred from the caller, even if unsuccessful.
+ * \param logFd a writable file descriptor for the log output, or -1. Ownership will always be
+ *   transferred from the caller, even if unsuccessful.
+ * \param vm an out parameter that will be set to the virtual machine handle.
+ *
+ * \return If successful, it sets `vm` and returns 0. Otherwise, it leaves `vm` untouched and
+ *   returns `-EIO`.
+ */
+int AVirtualMachine_createRaw(const AVirtualizationService* service,
+                              AVirtualMachineRawConfig* config, int consoleOutFd, int consoleInFd,
+                              int logFd, AVirtualMachine** vm);
+
+/**
+ * Start a virtual machine.
+ *
+ * \param vm a handle on a virtual machine.
+ *
+ * \return If successful, it returns 0. Otherwise, it returns `-EIO`.
+ */
+int AVirtualMachine_start(AVirtualMachine* vm);
+
+/**
+ * Stop a virtual machine.
+ *
+ * \param vm a handle on a virtual machine.
+ *
+ * \return If successful, it returns 0. Otherwise, it returns `-EIO`.
+ */
+int AVirtualMachine_stop(AVirtualMachine* vm);
+
+/**
+ * Wait until a virtual machine stops.
+ *
+ * \param vm a handle on a virtual machine.
+ *
+ * \return The reason why the virtual machine stopped.
+ */
+enum StopReason AVirtualMachine_waitForStop(AVirtualMachine* vm);
+
+/**
+ * Destroy a virtual machine.
+ *
+ * `AVirtualMachine_destroy` does nothing if `vm` is null. A destroyed virtual machine must not be
+ * reused.
+ *
+ * \param vm a handle on a virtual machine.
+ */
+void AVirtualMachine_destroy(AVirtualMachine* vm);
+
+__END_DECLS
diff --git a/libs/libavf/libavf.map.txt b/libs/libavf/libavf.map.txt
new file mode 100644
index 0000000..ecb4cc9
--- /dev/null
+++ b/libs/libavf/libavf.map.txt
@@ -0,0 +1,24 @@
+LIBAVF {
+  global:
+    AVirtualMachineRawConfig_create; # apex llndk
+    AVirtualMachineRawConfig_destroy; # apex llndk
+    AVirtualMachineRawConfig_setName; # apex llndk
+    AVirtualMachineRawConfig_setInstanceId; # apex llndk
+    AVirtualMachineRawConfig_setKernel; # apex llndk
+    AVirtualMachineRawConfig_setInitRd; # apex llndk
+    AVirtualMachineRawConfig_addDisk; # apex llndk
+    AVirtualMachineRawConfig_setMemoryMib; # apex llndk
+    AVirtualMachineRawConfig_setProtectedVm; # apex llndk
+    AVirtualMachineRawConfig_setBalloon; # apex llndk
+    AVirtualMachineRawConfig_setHypervisorSpecificAuthMethod; # apex llndk
+    AVirtualMachineRawConfig_addCustomMemoryBackingFile; # apex llndk
+    AVirtualizationService_create; # apex llndk
+    AVirtualizationService_destroy; # apex llndk
+    AVirtualMachine_createRaw; # apex llndk
+    AVirtualMachine_start; # apex llndk
+    AVirtualMachine_stop; # apex llndk
+    AVirtualMachine_waitForStop; # apex llndk
+    AVirtualMachine_destroy; # apex llndk
+  local:
+    *;
+};
diff --git a/libs/libavf/src/lib.rs b/libs/libavf/src/lib.rs
new file mode 100644
index 0000000..0a8f891
--- /dev/null
+++ b/libs/libavf/src/lib.rs
@@ -0,0 +1,413 @@
+// Copyright 2024 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.
+
+//! Stable C library for AVF.
+
+use std::ffi::CStr;
+use std::fs::File;
+use std::os::fd::FromRawFd;
+use std::os::raw::{c_char, c_int};
+use std::ptr;
+
+use android_system_virtualizationservice::{
+    aidl::android::system::virtualizationservice::{
+        DiskImage::DiskImage, IVirtualizationService::IVirtualizationService,
+        VirtualMachineConfig::VirtualMachineConfig,
+        VirtualMachineRawConfig::VirtualMachineRawConfig,
+    },
+    binder::{ParcelFileDescriptor, Strong},
+};
+use avf_bindgen::StopReason;
+use vmclient::{DeathReason, VirtualizationService, VmInstance};
+
+/// Create a new virtual machine config object with no properties.
+#[no_mangle]
+pub extern "C" fn AVirtualMachineRawConfig_create() -> *mut VirtualMachineRawConfig {
+    let config = Box::new(VirtualMachineRawConfig {
+        platformVersion: "~1.0".to_owned(),
+        ..Default::default()
+    });
+    Box::into_raw(config)
+}
+
+/// Destroy a virtual machine config object.
+///
+/// # Safety
+/// `config` must be a pointer returned by `AVirtualMachineRawConfig_create`. `config` must not be
+/// used after deletion.
+#[no_mangle]
+pub unsafe extern "C" fn AVirtualMachineRawConfig_destroy(config: *mut VirtualMachineRawConfig) {
+    if !config.is_null() {
+        // SAFETY: `config` is assumed to be a valid, non-null pointer returned by
+        // AVirtualMachineRawConfig_create. It's the only reference to the object.
+        unsafe {
+            let _ = Box::from_raw(config);
+        }
+    }
+}
+
+/// Set a name of a virtual machine.
+///
+/// # Safety
+/// `config` must be a pointer returned by `AVirtualMachineRawConfig_create`.
+#[no_mangle]
+pub unsafe extern "C" fn AVirtualMachineRawConfig_setName(
+    config: *mut VirtualMachineRawConfig,
+    name: *const c_char,
+) -> c_int {
+    // SAFETY: `config` is assumed to be a valid, non-null pointer returned by
+    // AVirtualMachineRawConfig_create. It's the only reference to the object.
+    let config = unsafe { &mut *config };
+    // SAFETY: `name` is assumed to be a pointer to a valid C string.
+    config.name = unsafe { CStr::from_ptr(name) }.to_string_lossy().into_owned();
+    0
+}
+
+/// Set an instance ID of a virtual machine.
+///
+/// # Safety
+/// `config` must be a pointer returned by `AVirtualMachineRawConfig_create`. `instanceId` must be a
+/// valid, non-null pointer to 64-byte data.
+#[no_mangle]
+pub unsafe extern "C" fn AVirtualMachineRawConfig_setInstanceId(
+    config: *mut VirtualMachineRawConfig,
+    instance_id: *const u8,
+) -> c_int {
+    // SAFETY: `config` is assumed to be a valid, non-null pointer returned by
+    // AVirtualMachineRawConfig_create. It's the only reference to the object.
+    let config = unsafe { &mut *config };
+    // SAFETY: `instanceId` is assumed to be a valid pointer to 64 bytes of memory. `config`
+    // is assumed to be a valid object returned by AVirtuaMachineConfig_create.
+    // Both never overlap.
+    unsafe {
+        ptr::copy_nonoverlapping(instance_id, config.instanceId.as_mut_ptr(), 64);
+    }
+    0
+}
+
+/// Set a kernel image of a virtual machine.
+///
+/// # Safety
+/// `config` must be a pointer returned by `AVirtualMachineRawConfig_create`. `fd` must be a valid
+/// file descriptor or -1. `AVirtualMachineRawConfig_setKernel` takes ownership of `fd` and `fd`
+/// will be closed upon `AVirtualMachineRawConfig_delete`.
+#[no_mangle]
+pub unsafe extern "C" fn AVirtualMachineRawConfig_setKernel(
+    config: *mut VirtualMachineRawConfig,
+    fd: c_int,
+) -> c_int {
+    let file = get_file_from_fd(fd);
+    // SAFETY: `config` is assumed to be a valid, non-null pointer returned by
+    // AVirtualMachineRawConfig_create. It's the only reference to the object.
+    let config = unsafe { &mut *config };
+    config.kernel = file.map(ParcelFileDescriptor::new);
+    0
+}
+
+/// Set an init rd of a virtual machine.
+///
+/// # Safety
+/// `config` must be a pointer returned by `AVirtualMachineRawConfig_create`. `fd` must be a valid
+/// file descriptor or -1. `AVirtualMachineRawConfig_setInitRd` takes ownership of `fd` and `fd`
+/// will be closed upon `AVirtualMachineRawConfig_delete`.
+#[no_mangle]
+pub unsafe extern "C" fn AVirtualMachineRawConfig_setInitRd(
+    config: *mut VirtualMachineRawConfig,
+    fd: c_int,
+) -> c_int {
+    let file = get_file_from_fd(fd);
+    // SAFETY: `config` is assumed to be a valid, non-null pointer returned by
+    // AVirtualMachineRawConfig_create. It's the only reference to the object.
+    let config = unsafe { &mut *config };
+    config.initrd = file.map(ParcelFileDescriptor::new);
+    0
+}
+
+/// Add a disk for a virtual machine.
+///
+/// # Safety
+/// `config` must be a pointer returned by `AVirtualMachineRawConfig_create`. `fd` must be a valid
+/// file descriptor. `AVirtualMachineRawConfig_addDisk` takes ownership of `fd` and `fd` will be
+/// closed upon `AVirtualMachineRawConfig_delete`.
+#[no_mangle]
+pub unsafe extern "C" fn AVirtualMachineRawConfig_addDisk(
+    config: *mut VirtualMachineRawConfig,
+    fd: c_int,
+    writable: bool,
+) -> c_int {
+    let file = get_file_from_fd(fd);
+    // SAFETY: `config` is assumed to be a valid, non-null pointer returned by
+    // AVirtualMachineRawConfig_create. It's the only reference to the object.
+    let config = unsafe { &mut *config };
+    match file {
+        // partition not supported yet
+        None => -libc::EINVAL,
+        Some(file) => {
+            config.disks.push(DiskImage {
+                image: Some(ParcelFileDescriptor::new(file)),
+                writable,
+                ..Default::default()
+            });
+            0
+        }
+    }
+}
+
+/// Set how much memory will be given to a virtual machine.
+///
+/// # Safety
+/// `config` must be a pointer returned by `AVirtualMachineRawConfig_create`.
+#[no_mangle]
+pub unsafe extern "C" fn AVirtualMachineRawConfig_setMemoryMib(
+    config: *mut VirtualMachineRawConfig,
+    memory_mib: i32,
+) -> c_int {
+    // SAFETY: `config` is assumed to be a valid, non-null pointer returned by
+    // AVirtualMachineRawConfig_create. It's the only reference to the object.
+    let config = unsafe { &mut *config };
+    config.memoryMib = memory_mib;
+    0
+}
+
+/// Set whether a virtual machine is protected or not.
+///
+/// # Safety
+/// `config` must be a pointer returned by `AVirtualMachineRawConfig_create`.
+#[no_mangle]
+pub unsafe extern "C" fn AVirtualMachineRawConfig_setProtectedVm(
+    config: *mut VirtualMachineRawConfig,
+    protected_vm: bool,
+) -> c_int {
+    // SAFETY: `config` is assumed to be a valid, non-null pointer returned by
+    // AVirtualMachineRawConfig_create. It's the only reference to the object.
+    let config = unsafe { &mut *config };
+    config.protectedVm = protected_vm;
+    0
+}
+
+/// Set whether a virtual machine uses memory ballooning or not.
+///
+/// # Safety
+/// `config` must be a pointer returned by `AVirtualMachineRawConfig_create`.
+#[no_mangle]
+pub unsafe extern "C" fn AVirtualMachineRawConfig_setBalloon(
+    config: *mut VirtualMachineRawConfig,
+    balloon: bool,
+) -> c_int {
+    // SAFETY: `config` is assumed to be a valid, non-null pointer returned by
+    // AVirtualMachineRawConfig_create. It's the only reference to the object.
+    let config = unsafe { &mut *config };
+    config.noBalloon = !balloon;
+    0
+}
+
+/// NOT IMPLEMENTED.
+///
+/// # Returns
+/// It always returns `-ENOTSUP`.
+#[no_mangle]
+pub extern "C" fn AVirtualMachineRawConfig_setHypervisorSpecificAuthMethod(
+    _config: *mut VirtualMachineRawConfig,
+    _enable: bool,
+) -> c_int {
+    -libc::ENOTSUP
+}
+
+/// NOT IMPLEMENTED.
+///
+/// # Returns
+/// It always returns `-ENOTSUP`.
+#[no_mangle]
+pub extern "C" fn AVirtualMachineRawConfig_addCustomMemoryBackingFile(
+    _config: *mut VirtualMachineRawConfig,
+    _fd: c_int,
+    _range_start: usize,
+    _range_end: usize,
+) -> c_int {
+    -libc::ENOTSUP
+}
+
+/// Spawn a new instance of `virtmgr`, a child process that will host the `VirtualizationService`
+/// AIDL service, and connect to the child process.
+///
+/// # Safety
+/// `service_ptr` must be a valid, non-null pointer to a mutable raw pointer.
+#[no_mangle]
+pub unsafe extern "C" fn AVirtualizationService_create(
+    service_ptr: *mut *mut Strong<dyn IVirtualizationService>,
+    early: bool,
+) -> c_int {
+    let virtmgr =
+        if early { VirtualizationService::new_early() } else { VirtualizationService::new() };
+    let virtmgr = match virtmgr {
+        Ok(virtmgr) => virtmgr,
+        Err(e) => return -e.raw_os_error().unwrap_or(libc::EIO),
+    };
+    match virtmgr.connect() {
+        Ok(service) => {
+            // SAFETY: `service` is assumed to be a valid, non-null pointer to a mutable raw
+            // pointer. `service` is the only reference here and `config` takes
+            // ownership.
+            unsafe {
+                *service_ptr = Box::into_raw(Box::new(service));
+            }
+            0
+        }
+        Err(_) => -libc::ECONNREFUSED,
+    }
+}
+
+/// Destroy a VirtualizationService object.
+///
+/// # Safety
+/// `service` must be a pointer returned by `AVirtualizationService_create` or
+/// `AVirtualizationService_create_early`. `service` must not be reused after deletion.
+#[no_mangle]
+pub unsafe extern "C" fn AVirtualizationService_destroy(
+    service: *mut Strong<dyn IVirtualizationService>,
+) {
+    if !service.is_null() {
+        // SAFETY: `service` is assumed to be a valid, non-null pointer returned by
+        // `AVirtualizationService_create`. It's the only reference to the object.
+        unsafe {
+            let _ = Box::from_raw(service);
+        }
+    }
+}
+
+/// Create a virtual machine with given `config`.
+///
+/// # Safety
+/// `config` must be a pointer returned by `AVirtualMachineRawConfig_create`. `service` must be a
+/// pointer returned by `AVirtualMachineRawConfig_create`. `vm_ptr` must be a valid, non-null
+/// pointer to a mutable raw pointer. `console_out_fd`, `console_in_fd`, and `log_fd` must be a
+/// valid file descriptor or -1. `AVirtualMachine_create` takes ownership of `console_out_fd`,
+/// `console_in_fd`, and `log_fd`, and taken file descriptors must not be reused.
+#[no_mangle]
+pub unsafe extern "C" fn AVirtualMachine_createRaw(
+    service: *const Strong<dyn IVirtualizationService>,
+    config: *mut VirtualMachineRawConfig,
+    console_out_fd: c_int,
+    console_in_fd: c_int,
+    log_fd: c_int,
+    vm_ptr: *mut *mut VmInstance,
+) -> c_int {
+    // SAFETY: `service` is assumed to be a valid, non-null pointer returned by
+    // `AVirtualizationService_create` or `AVirtualizationService_create_early`. It's the only
+    // reference to the object.
+    let service = unsafe { &*service };
+
+    // SAFETY: `config` is assumed to be a valid, non-null pointer returned by
+    // `AVirtualMachineRawConfig_create`. It's the only reference to the object.
+    let config = unsafe { *Box::from_raw(config) };
+    let config = VirtualMachineConfig::RawConfig(config);
+
+    let console_out = get_file_from_fd(console_out_fd);
+    let console_in = get_file_from_fd(console_in_fd);
+    let log = get_file_from_fd(log_fd);
+
+    match VmInstance::create(service.as_ref(), &config, console_out, console_in, log, None, None) {
+        Ok(vm) => {
+            // SAFETY: `vm_ptr` is assumed to be a valid, non-null pointer to a mutable raw pointer.
+            // `vm` is the only reference here and `vm_ptr` takes ownership.
+            unsafe {
+                *vm_ptr = Box::into_raw(Box::new(vm));
+            }
+            0
+        }
+        Err(_) => -libc::EIO,
+    }
+}
+
+/// Start a virtual machine.
+///
+/// # Safety
+/// `vm` must be a pointer returned by `AVirtualMachine_createRaw`.
+#[no_mangle]
+pub unsafe extern "C" fn AVirtualMachine_start(vm: *const VmInstance) -> c_int {
+    // SAFETY: `vm` is assumed to be a valid, non-null pointer returned by
+    // `AVirtualMachine_createRaw`. It's the only reference to the object.
+    let vm = unsafe { &*vm };
+    match vm.start() {
+        Ok(_) => 0,
+        Err(_) => -libc::EIO,
+    }
+}
+
+/// Stop a virtual machine.
+///
+/// # Safety
+/// `vm` must be a pointer returned by `AVirtualMachine_create`.
+#[no_mangle]
+pub unsafe extern "C" fn AVirtualMachine_stop(vm: *const VmInstance) -> c_int {
+    // SAFETY: `vm` is assumed to be a valid, non-null pointer returned by
+    // `AVirtualMachine_createRaw`. It's the only reference to the object.
+    let vm = unsafe { &*vm };
+    match vm.stop() {
+        Ok(_) => 0,
+        Err(_) => -libc::EIO,
+    }
+}
+
+/// Wait until a virtual machine stops.
+///
+/// # Safety
+/// `vm` must be a pointer returned by `AVirtualMachine_createRaw`.
+#[no_mangle]
+pub unsafe extern "C" fn AVirtualMachine_waitForStop(vm: *const VmInstance) -> StopReason {
+    // SAFETY: `vm` is assumed to be a valid, non-null pointer returned by
+    // AVirtualMachine_create. It's the only reference to the object.
+    let vm = unsafe { &*vm };
+    match vm.wait_for_death() {
+        DeathReason::VirtualizationServiceDied => StopReason::VIRTUALIZATION_SERVICE_DIED,
+        DeathReason::InfrastructureError => StopReason::INFRASTRUCTURE_ERROR,
+        DeathReason::Killed => StopReason::KILLED,
+        DeathReason::Unknown => StopReason::UNKNOWN,
+        DeathReason::Shutdown => StopReason::SHUTDOWN,
+        DeathReason::StartFailed => StopReason::START_FAILED,
+        DeathReason::Reboot => StopReason::REBOOT,
+        DeathReason::Crash => StopReason::CRASH,
+        DeathReason::PvmFirmwarePublicKeyMismatch => StopReason::PVM_FIRMWARE_PUBLIC_KEY_MISMATCH,
+        DeathReason::PvmFirmwareInstanceImageChanged => {
+            StopReason::PVM_FIRMWARE_INSTANCE_IMAGE_CHANGED
+        }
+        DeathReason::Hangup => StopReason::HANGUP,
+        _ => StopReason::UNRECOGNISED,
+    }
+}
+
+/// Destroy a virtual machine.
+///
+/// # Safety
+/// `vm` must be a pointer returned by `AVirtualMachine_createRaw`. `vm` must not be reused after
+/// deletion.
+#[no_mangle]
+pub unsafe extern "C" fn AVirtualMachine_destroy(vm: *mut VmInstance) {
+    if !vm.is_null() {
+        // SAFETY: `vm` is assumed to be a valid, non-null pointer returned by
+        // AVirtualMachine_create. It's the only reference to the object.
+        unsafe {
+            let _ = Box::from_raw(vm);
+        }
+    }
+}
+
+fn get_file_from_fd(fd: i32) -> Option<File> {
+    if fd == -1 {
+        None
+    } else {
+        // SAFETY: transferring ownership of `fd` from the caller
+        Some(unsafe { File::from_raw_fd(fd) })
+    }
+}
diff --git a/libs/libvmbase/sections.ld b/libs/libvmbase/sections.ld
index 222edae..9d69935 100644
--- a/libs/libvmbase/sections.ld
+++ b/libs/libvmbase/sections.ld
@@ -132,3 +132,10 @@
 		*(.note.gnu.build-id)
 	}
 }
+
+/*
+ * Make calling the limit_stack_size!() macro optional by providing a default.
+ */
+PROVIDE(vmbase_stack_limit = DEFINED(vmbase_stack_limit_client) ?
+                                     vmbase_stack_limit_client :
+                                     vmbase_stack_limit_default);
diff --git a/libs/libvmbase/src/entry.rs b/libs/libvmbase/src/entry.rs
index 2433722..b681aea 100644
--- a/libs/libvmbase/src/entry.rs
+++ b/libs/libvmbase/src/entry.rs
@@ -18,7 +18,7 @@
     bionic, console, heap,
     layout::{UART_ADDRESSES, UART_PAGE_ADDR},
     logger,
-    memory::{PAGE_SIZE, SIZE_16KB, SIZE_4KB},
+    memory::{switch_to_dynamic_page_tables, PAGE_SIZE, SIZE_16KB, SIZE_4KB},
     power::{reboot, shutdown},
     rand,
 };
@@ -82,6 +82,8 @@
 
     bionic::__get_tls().stack_guard = u64::from_ne_bytes(stack_guard);
 
+    switch_to_dynamic_page_tables();
+
     // 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.
 
diff --git a/libs/libvmbase/src/layout.rs b/libs/libvmbase/src/layout.rs
index a8f7827..9a702b0 100644
--- a/libs/libvmbase/src/layout.rs
+++ b/libs/libvmbase/src/layout.rs
@@ -17,7 +17,7 @@
 pub mod crosvm;
 
 use crate::linker::__stack_chk_guard;
-use crate::memory::{page_4kb_of, PAGE_SIZE};
+use crate::memory::{max_stack_size, page_4kb_of, PAGE_SIZE};
 use aarch64_paging::paging::VirtualAddress;
 use core::ops::Range;
 use core::ptr::addr_of;
@@ -91,10 +91,16 @@
 }
 
 /// Writable data region for the stack.
-pub fn stack_range(stack_size: usize) -> Range<VirtualAddress> {
+pub fn stack_range() -> Range<VirtualAddress> {
     let end = linker_addr!(init_stack_pointer);
-    let start = VirtualAddress(end.0.checked_sub(stack_size).unwrap());
-    assert!(start >= linker_addr!(stack_limit));
+    let start = if let Some(stack_size) = max_stack_size() {
+        assert_eq!(stack_size % PAGE_SIZE, 0);
+        let start = VirtualAddress(end.0.checked_sub(stack_size).unwrap());
+        assert!(start >= linker_addr!(stack_limit));
+        start
+    } else {
+        linker_addr!(stack_limit)
+    };
 
     start..end
 }
diff --git a/libs/libvmbase/src/memory.rs b/libs/libvmbase/src/memory.rs
index e0ea207..fd4706f 100644
--- a/libs/libvmbase/src/memory.rs
+++ b/libs/libvmbase/src/memory.rs
@@ -18,17 +18,24 @@
 mod error;
 mod page_table;
 mod shared;
+mod stack;
 mod tracker;
 mod util;
 
 pub use error::MemoryTrackerError;
 pub use page_table::PageTable;
 pub use shared::MemoryRange;
-pub use tracker::{MemoryTracker, MEMORY};
+pub use tracker::{
+    deactivate_dynamic_page_tables, init_shared_pool, map_data, map_device, map_image_footer,
+    map_rodata, map_rodata_outside_main_memory, resize_available_memory, unshare_all_memory,
+    unshare_all_mmio_except_uart, unshare_uart,
+};
 pub use util::{
     flush, flushed_zeroize, page_4kb_of, PAGE_SIZE, SIZE_128KB, SIZE_16KB, SIZE_2MB, SIZE_4KB,
     SIZE_4MB, SIZE_64KB,
 };
 
 pub(crate) use shared::{alloc_shared, dealloc_shared};
+pub(crate) use stack::max_stack_size;
+pub(crate) use tracker::{switch_to_dynamic_page_tables, MEMORY};
 pub(crate) use util::{phys_to_virt, virt_to_phys};
diff --git a/libs/libvmbase/src/memory/error.rs b/libs/libvmbase/src/memory/error.rs
index 870e4c9..2c00518 100644
--- a/libs/libvmbase/src/memory/error.rs
+++ b/libs/libvmbase/src/memory/error.rs
@@ -21,6 +21,8 @@
 /// Errors for MemoryTracker operations.
 #[derive(Debug, Clone)]
 pub enum MemoryTrackerError {
+    /// MemoryTracker not configured or deactivated.
+    Unavailable,
     /// Tried to modify the memory base address.
     DifferentBaseAddress,
     /// Tried to shrink to a larger memory size.
@@ -43,6 +45,8 @@
     SharedMemorySetFailure,
     /// Failure to set `SHARED_POOL`.
     SharedPoolSetFailure,
+    /// Rejected request to map footer that is already mapped.
+    FooterAlreadyMapped,
     /// Invalid page table entry.
     InvalidPte,
     /// Failed to flush memory region.
@@ -58,6 +62,7 @@
 impl fmt::Display for MemoryTrackerError {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         match self {
+            Self::Unavailable => write!(f, "MemoryTracker is not available"),
             Self::DifferentBaseAddress => write!(f, "Received different base address"),
             Self::SizeTooLarge => write!(f, "Tried to shrink to a larger memory size"),
             Self::SizeTooSmall => write!(f, "Tracked regions would not fit in memory size"),
@@ -69,6 +74,7 @@
             Self::Hypervisor(e) => e.fmt(f),
             Self::SharedMemorySetFailure => write!(f, "Failed to set SHARED_MEMORY"),
             Self::SharedPoolSetFailure => write!(f, "Failed to set SHARED_POOL"),
+            Self::FooterAlreadyMapped => write!(f, "Refused to map image footer again"),
             Self::InvalidPte => write!(f, "Page table entry is not valid"),
             Self::FlushRegionFailed => write!(f, "Failed to flush memory region"),
             Self::SetPteDirtyFailed => write!(f, "Failed to set PTE dirty state"),
diff --git a/libs/libvmbase/src/memory/stack.rs b/libs/libvmbase/src/memory/stack.rs
new file mode 100644
index 0000000..639029e
--- /dev/null
+++ b/libs/libvmbase/src/memory/stack.rs
@@ -0,0 +1,41 @@
+// Copyright 2024, 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.
+
+//! Low-level stack support.
+
+/// Configures the maximum size of the stack.
+#[macro_export]
+macro_rules! limit_stack_size {
+    ($len:expr) => {
+        #[export_name = "vmbase_stack_limit_client"]
+        fn __vmbase_stack_limit_client() -> Option<usize> {
+            Some($len)
+        }
+    };
+}
+
+pub(crate) fn max_stack_size() -> Option<usize> {
+    extern "Rust" {
+        fn vmbase_stack_limit() -> Option<usize>;
+    }
+    // SAFETY: This function is safe to call as the linker script aliases it to either:
+    // - the safe vmbase_stack_limit_default();
+    // - the safe vmbase_stack_limit_client() potentially defined using limit_stack_size!()
+    unsafe { vmbase_stack_limit() }
+}
+
+#[no_mangle]
+fn vmbase_stack_limit_default() -> Option<usize> {
+    None
+}
diff --git a/libs/libvmbase/src/memory/tracker.rs b/libs/libvmbase/src/memory/tracker.rs
index c1f5d54..3416dc6 100644
--- a/libs/libvmbase/src/memory/tracker.rs
+++ b/libs/libvmbase/src/memory/tracker.rs
@@ -19,6 +19,7 @@
 use super::page_table::{PageTable, MMIO_LAZY_MAP_FLAG};
 use super::shared::{SHARED_MEMORY, SHARED_POOL};
 use crate::dsb;
+use crate::layout;
 use crate::memory::shared::{MemoryRange, MemorySharer, MmioSharer};
 use crate::util::RangeExt as _;
 use aarch64_paging::paging::{Attributes, Descriptor, MemoryRegion as VaRange, VirtualAddress};
@@ -28,13 +29,13 @@
 use core::num::NonZeroUsize;
 use core::ops::Range;
 use core::result;
-use hypervisor_backends::get_mmio_guard;
-use log::{debug, error};
-use spin::mutex::SpinMutex;
+use hypervisor_backends::{get_mem_sharer, get_mmio_guard};
+use log::{debug, error, info};
+use spin::mutex::{SpinMutex, SpinMutexGuard};
 use tinyvec::ArrayVec;
 
 /// A global static variable representing the system memory tracker, protected by a spin mutex.
-pub static MEMORY: SpinMutex<Option<MemoryTracker>> = SpinMutex::new(None);
+pub(crate) static MEMORY: SpinMutex<Option<MemoryTracker>> = SpinMutex::new(None);
 
 fn get_va_range(range: &MemoryRange) -> VaRange {
     VaRange::new(range.start, range.end)
@@ -42,6 +43,140 @@
 
 type Result<T> = result::Result<T, MemoryTrackerError>;
 
+/// Attempts to lock `MEMORY`, returns an error if already deactivated.
+fn try_lock_memory_tracker() -> Result<SpinMutexGuard<'static, Option<MemoryTracker>>> {
+    // Being single-threaded, we only spin if `deactivate_dynamic_page_tables()` leaked the lock.
+    MEMORY.try_lock().ok_or(MemoryTrackerError::Unavailable)
+}
+
+/// Switch the MMU to the provided PageTable.
+///
+/// Panics if called more than once.
+pub(crate) fn switch_to_dynamic_page_tables() {
+    let mut locked_tracker = try_lock_memory_tracker().unwrap();
+    if locked_tracker.is_some() {
+        panic!("switch_to_dynamic_page_tables() called more than once.");
+    }
+
+    locked_tracker.replace(MemoryTracker::new(
+        layout::crosvm::MEM_START..layout::MAX_VIRT_ADDR,
+        layout::crosvm::MMIO_RANGE,
+    ));
+}
+
+/// Switch the MMU back to the static page tables (see `idmap` C symbol).
+///
+/// Panics if called before `switch_to_dynamic_page_tables()` or more than once.
+pub fn deactivate_dynamic_page_tables() {
+    let locked_tracker = try_lock_memory_tracker().unwrap();
+    // Force future calls to try_lock_memory_tracker() to fail by leaking this lock guard.
+    let leaked_tracker = SpinMutexGuard::leak(locked_tracker);
+    // Force deallocation/unsharing of all the resources used by the MemoryTracker.
+    drop(leaked_tracker.take())
+}
+
+/// Redefines the actual mappable range of memory.
+///
+/// Fails if a region has already been mapped beyond the new upper limit.
+pub fn resize_available_memory(memory_range: &Range<usize>) -> Result<()> {
+    let mut locked_tracker = try_lock_memory_tracker()?;
+    let tracker = locked_tracker.as_mut().ok_or(MemoryTrackerError::Unavailable)?;
+    tracker.shrink(memory_range)
+}
+
+/// Initialize the memory pool for page sharing with the host.
+pub fn init_shared_pool(static_range: Option<Range<usize>>) -> Result<()> {
+    let mut locked_tracker = try_lock_memory_tracker()?;
+    let tracker = locked_tracker.as_mut().ok_or(MemoryTrackerError::Unavailable)?;
+    if let Some(mem_sharer) = get_mem_sharer() {
+        let granule = mem_sharer.granule()?;
+        tracker.init_dynamic_shared_pool(granule)
+    } else if let Some(r) = static_range {
+        tracker.init_static_shared_pool(r)
+    } else {
+        info!("Initialized shared pool from heap memory without MEM_SHARE");
+        tracker.init_heap_shared_pool()
+    }
+}
+
+/// Unshare all MMIO that was previously shared with the host, with the exception of the UART page.
+pub fn unshare_all_mmio_except_uart() -> Result<()> {
+    let Ok(mut locked_tracker) = try_lock_memory_tracker() else { return Ok(()) };
+    let Some(tracker) = locked_tracker.as_mut() else { return Ok(()) };
+    if cfg!(feature = "compat_android_13") {
+        info!("Expecting a bug making MMIO_GUARD_UNMAP return NOT_SUPPORTED on success");
+    }
+    tracker.unshare_all_mmio()
+}
+
+/// Unshare all memory that was previously shared with the host.
+pub fn unshare_all_memory() {
+    let Ok(mut locked_tracker) = try_lock_memory_tracker() else { return };
+    let Some(tracker) = locked_tracker.as_mut() else { return };
+    tracker.unshare_all_memory()
+}
+
+/// Unshare the UART page, previously shared with the host.
+pub fn unshare_uart() -> Result<()> {
+    let Some(mmio_guard) = get_mmio_guard() else { return Ok(()) };
+    Ok(mmio_guard.unmap(layout::UART_PAGE_ADDR)?)
+}
+
+/// Map the provided range as normal memory, with R/W permissions.
+///
+/// This fails if the range has already been (partially) mapped.
+pub fn map_data(addr: usize, size: NonZeroUsize) -> Result<()> {
+    let mut locked_tracker = try_lock_memory_tracker()?;
+    let tracker = locked_tracker.as_mut().ok_or(MemoryTrackerError::Unavailable)?;
+    let _ = tracker.alloc_mut(addr, size)?;
+    Ok(())
+}
+
+/// Map the region potentially holding data appended to the image, with read-write permissions.
+///
+/// This fails if the footer has already been mapped.
+pub fn map_image_footer() -> Result<Range<usize>> {
+    let mut locked_tracker = try_lock_memory_tracker()?;
+    let tracker = locked_tracker.as_mut().ok_or(MemoryTrackerError::Unavailable)?;
+    let range = tracker.map_image_footer()?;
+    Ok(range)
+}
+
+/// Map the provided range as normal memory, with read-only permissions.
+///
+/// This fails if the range has already been (partially) mapped.
+pub fn map_rodata(addr: usize, size: NonZeroUsize) -> Result<()> {
+    let mut locked_tracker = try_lock_memory_tracker()?;
+    let tracker = locked_tracker.as_mut().ok_or(MemoryTrackerError::Unavailable)?;
+    let _ = tracker.alloc(addr, size)?;
+    Ok(())
+}
+
+// TODO(ptosi): Merge this into map_rodata.
+/// Map the provided range as normal memory, with read-only permissions.
+///
+/// # Safety
+///
+/// Callers of this method need to ensure that the `range` is valid for mapping as read-only data.
+pub unsafe fn map_rodata_outside_main_memory(addr: usize, size: NonZeroUsize) -> Result<()> {
+    let mut locked_tracker = try_lock_memory_tracker()?;
+    let tracker = locked_tracker.as_mut().ok_or(MemoryTrackerError::Unavailable)?;
+    let end = addr + usize::from(size);
+    // SAFETY: Caller has checked that it is valid to map the range.
+    let _ = unsafe { tracker.alloc_range_outside_main_memory(&(addr..end)) }?;
+    Ok(())
+}
+
+/// Map the provided range as device memory.
+///
+/// This fails if the range has already been (partially) mapped.
+pub fn map_device(addr: usize, size: NonZeroUsize) -> Result<()> {
+    let mut locked_tracker = try_lock_memory_tracker()?;
+    let tracker = locked_tracker.as_mut().ok_or(MemoryTrackerError::Unavailable)?;
+    let range = addr..(addr + usize::from(size));
+    tracker.map_mmio_range(range.clone())
+}
+
 #[derive(Clone, Copy, Debug, Default, PartialEq)]
 enum MemoryType {
     #[default]
@@ -56,13 +191,13 @@
 }
 
 /// Tracks non-overlapping slices of main memory.
-pub struct MemoryTracker {
+pub(crate) struct MemoryTracker {
     total: MemoryRange,
     page_table: PageTable,
     regions: ArrayVec<[MemoryRegion; MemoryTracker::CAPACITY]>,
     mmio_regions: ArrayVec<[MemoryRange; MemoryTracker::MMIO_CAPACITY]>,
     mmio_range: MemoryRange,
-    payload_range: Option<MemoryRange>,
+    image_footer_mapped: bool,
     mmio_sharer: MmioSharer,
 }
 
@@ -71,17 +206,13 @@
     const MMIO_CAPACITY: usize = 5;
 
     /// Creates a new instance from an active page table, covering the maximum RAM size.
-    pub fn new(
-        mut page_table: PageTable,
-        total: MemoryRange,
-        mmio_range: MemoryRange,
-        payload_range: Option<Range<VirtualAddress>>,
-    ) -> Self {
+    fn new(total: MemoryRange, mmio_range: MemoryRange) -> Self {
         assert!(
             !total.overlaps(&mmio_range),
             "MMIO space should not overlap with the main memory region."
         );
 
+        let mut page_table = Self::initialize_dynamic_page_tables();
         // Activate dirty state management first, otherwise we may get permission faults immediately
         // after activating the new page table. This has no effect before the new page table is
         // activated because none of the entries in the initial idmap have the DBM flag.
@@ -99,7 +230,7 @@
             regions: ArrayVec::new(),
             mmio_regions: ArrayVec::new(),
             mmio_range,
-            payload_range: payload_range.map(|r| r.start.0..r.end.0),
+            image_footer_mapped: false,
             mmio_sharer: MmioSharer::new().unwrap(),
         }
     }
@@ -107,7 +238,7 @@
     /// Resize the total RAM size.
     ///
     /// This function fails if it contains regions that are not included within the new size.
-    pub fn shrink(&mut self, range: &MemoryRange) -> Result<()> {
+    fn shrink(&mut self, range: &MemoryRange) -> Result<()> {
         if range.start != self.total.start {
             return Err(MemoryTrackerError::DifferentBaseAddress);
         }
@@ -123,7 +254,7 @@
     }
 
     /// Allocate the address range for a const slice; returns None if failed.
-    pub fn alloc_range(&mut self, range: &MemoryRange) -> Result<MemoryRange> {
+    fn alloc_range(&mut self, range: &MemoryRange) -> Result<MemoryRange> {
         let region = MemoryRegion { range: range.clone(), mem_type: MemoryType::ReadOnly };
         self.check_allocatable(&region)?;
         self.page_table.map_rodata(&get_va_range(range)).map_err(|e| {
@@ -139,7 +270,7 @@
     ///
     /// Callers of this method need to ensure that the `range` is valid for mapping as read-only
     /// data.
-    pub unsafe fn alloc_range_outside_main_memory(
+    unsafe fn alloc_range_outside_main_memory(
         &mut self,
         range: &MemoryRange,
     ) -> Result<MemoryRange> {
@@ -153,7 +284,7 @@
     }
 
     /// Allocate the address range for a mutable slice; returns None if failed.
-    pub fn alloc_range_mut(&mut self, range: &MemoryRange) -> Result<MemoryRange> {
+    fn alloc_range_mut(&mut self, range: &MemoryRange) -> Result<MemoryRange> {
         let region = MemoryRegion { range: range.clone(), mem_type: MemoryType::ReadWrite };
         self.check_allocatable(&region)?;
         self.page_table.map_data_dbm(&get_va_range(range)).map_err(|e| {
@@ -163,19 +294,33 @@
         self.add(region)
     }
 
+    /// Maps the image footer read-write, with permissions.
+    fn map_image_footer(&mut self) -> Result<MemoryRange> {
+        if self.image_footer_mapped {
+            return Err(MemoryTrackerError::FooterAlreadyMapped);
+        }
+        let range = layout::image_footer_range();
+        self.page_table.map_data_dbm(&range.clone().into()).map_err(|e| {
+            error!("Error during image footer map: {e}");
+            MemoryTrackerError::FailedToMap
+        })?;
+        self.image_footer_mapped = true;
+        Ok(range.start.0..range.end.0)
+    }
+
     /// Allocate the address range for a const slice; returns None if failed.
-    pub fn alloc(&mut self, base: usize, size: NonZeroUsize) -> Result<MemoryRange> {
+    fn alloc(&mut self, base: usize, size: NonZeroUsize) -> Result<MemoryRange> {
         self.alloc_range(&(base..(base + size.get())))
     }
 
     /// Allocate the address range for a mutable slice; returns None if failed.
-    pub fn alloc_mut(&mut self, base: usize, size: NonZeroUsize) -> Result<MemoryRange> {
+    fn alloc_mut(&mut self, base: usize, size: NonZeroUsize) -> Result<MemoryRange> {
         self.alloc_range_mut(&(base..(base + size.get())))
     }
 
     /// Checks that the given range of addresses is within the MMIO region, and then maps it
     /// appropriately.
-    pub fn map_mmio_range(&mut self, range: MemoryRange) -> Result<()> {
+    fn map_mmio_range(&mut self, range: MemoryRange) -> Result<()> {
         if !range.is_within(&self.mmio_range) {
             return Err(MemoryTrackerError::OutOfRange);
         }
@@ -237,14 +382,14 @@
     }
 
     /// Unshares any MMIO region previously shared with the MMIO guard.
-    pub fn unshare_all_mmio(&mut self) -> Result<()> {
+    fn unshare_all_mmio(&mut self) -> Result<()> {
         self.mmio_sharer.unshare_all();
 
         Ok(())
     }
 
     /// Initialize the shared heap to dynamically share memory from the global allocator.
-    pub fn init_dynamic_shared_pool(&mut self, granule: usize) -> Result<()> {
+    fn init_dynamic_shared_pool(&mut self, granule: usize) -> Result<()> {
         const INIT_CAP: usize = 10;
 
         let previous = SHARED_MEMORY.lock().replace(MemorySharer::new(granule, INIT_CAP));
@@ -266,7 +411,7 @@
     /// of guest memory as "shared" ahead of guest starting its execution. The
     /// shared memory region is indicated in swiotlb node. On such platforms use
     /// a separate heap to allocate buffers that can be shared with host.
-    pub fn init_static_shared_pool(&mut self, range: Range<usize>) -> Result<()> {
+    fn init_static_shared_pool(&mut self, range: Range<usize>) -> Result<()> {
         let size = NonZeroUsize::new(range.len()).unwrap();
         let range = self.alloc_mut(range.start, size)?;
         let shared_pool = LockedFrameAllocator::<32>::new();
@@ -285,7 +430,7 @@
     /// 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<()> {
+    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
@@ -336,11 +481,17 @@
         // observed before reading PTE flags to determine dirty state.
         dsb!("ish");
         // Now flush writable-dirty pages in those regions.
-        for range in writable_regions.chain(self.payload_range.as_ref().into_iter()) {
+        for range in writable_regions {
             self.page_table
                 .walk_range(&get_va_range(range), &flush_dirty_range)
                 .map_err(|_| MemoryTrackerError::FlushRegionFailed)?;
         }
+        if self.image_footer_mapped {
+            let range = layout::image_footer_range();
+            self.page_table
+                .walk_range(&range.into(), &flush_dirty_range)
+                .map_err(|_| MemoryTrackerError::FlushRegionFailed)?;
+        }
         Ok(())
     }
 
@@ -352,6 +503,28 @@
             .modify_range(&(addr..addr + 1).into(), &mark_dirty_block)
             .map_err(|_| MemoryTrackerError::SetPteDirtyFailed)
     }
+
+    // TODO(ptosi): Move this and `PageTable` references to crate::arch::aarch64
+    /// Produces a `PageTable` that can safely replace the static PTs.
+    fn initialize_dynamic_page_tables() -> PageTable {
+        let text = layout::text_range();
+        let rodata = layout::rodata_range();
+        let data_bss = layout::data_bss_range();
+        let eh_stack = layout::eh_stack_range();
+        let stack = layout::stack_range();
+        let console_uart_page = layout::console_uart_page();
+
+        let mut page_table = PageTable::default();
+
+        page_table.map_device(&console_uart_page.into()).unwrap();
+        page_table.map_code(&text.into()).unwrap();
+        page_table.map_rodata(&rodata.into()).unwrap();
+        page_table.map_data(&data_bss.into()).unwrap();
+        page_table.map_data(&eh_stack.into()).unwrap();
+        page_table.map_data(&stack.into()).unwrap();
+
+        page_table
+    }
 }
 
 impl Drop for MemoryTracker {
diff --git a/libs/libvmbase/src/virtio/pci.rs b/libs/libvmbase/src/virtio/pci.rs
index 72e648b..ec89b6b 100644
--- a/libs/libvmbase/src/virtio/pci.rs
+++ b/libs/libvmbase/src/virtio/pci.rs
@@ -16,7 +16,7 @@
 
 use crate::{
     fdt::pci::PciInfo,
-    memory::{MemoryTracker, MemoryTrackerError},
+    memory::{map_device, MemoryTrackerError},
 };
 use alloc::boxed::Box;
 use core::fmt;
@@ -65,16 +65,19 @@
 /// 2. Stores the `PciInfo` for the VirtIO HAL to use later.
 /// 3. Creates and returns a `PciRoot`.
 ///
-/// This must only be called once; it will panic if it is called a second time.
-pub fn initialize(pci_info: PciInfo, memory: &mut MemoryTracker) -> Result<PciRoot, PciError> {
+/// This must only be called once and after having switched to the dynamic page tables.
+pub fn initialize(pci_info: PciInfo) -> Result<PciRoot, PciError> {
     PCI_INFO.set(Box::new(pci_info.clone())).map_err(|_| PciError::DuplicateInitialization)?;
 
-    memory.map_mmio_range(pci_info.cam_range.clone()).map_err(PciError::CamMapFailed)?;
-    let bar_range = pci_info.bar_range.start as usize..pci_info.bar_range.end as usize;
-    memory.map_mmio_range(bar_range).map_err(PciError::BarMapFailed)?;
+    let cam_start = pci_info.cam_range.start;
+    let cam_size = pci_info.cam_range.len().try_into().unwrap();
+    map_device(cam_start, cam_size).map_err(PciError::CamMapFailed)?;
 
-    // Safety: This is the only place where we call make_pci_root, and `PCI_INFO.set` above will
-    // panic if it is called a second time.
+    let bar_start = pci_info.bar_range.start.try_into().unwrap();
+    let bar_size = pci_info.bar_range.len().try_into().unwrap();
+    map_device(bar_start, bar_size).map_err(PciError::BarMapFailed)?;
+
+    // SAFETY: This is the only place where we call make_pci_root, validated by `PCI_INFO.set`.
     Ok(unsafe { pci_info.make_pci_root() })
 }
 
diff --git a/libs/libvmclient/src/lib.rs b/libs/libvmclient/src/lib.rs
index 13630c0..c0baea5 100644
--- a/libs/libvmclient/src/lib.rs
+++ b/libs/libvmclient/src/lib.rs
@@ -243,6 +243,11 @@
         self.vm.start()
     }
 
+    /// Stops the VM.
+    pub fn stop(&self) -> BinderResult<()> {
+        self.vm.stop()
+    }
+
     /// Returns the CID used for vsock connections to the VM.
     pub fn cid(&self) -> i32 {
         self.cid