Merge "Update is_nested_virtualization() to check for qemu" into main
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.java b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.java
index c57c4c0..0ffc093 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.java
@@ -256,7 +256,7 @@
                             // ttyd name it as "Terminal input" but it's not i18n'ed. Override it
                             // here for better i18n.
                             info.setText(null);
-                            info.setHintText(null);
+                            info.setHintText(getString(R.string.double_tap_to_edit_text));
                             info.setContentDescription(getString(R.string.terminal_input));
                             info.setScreenReaderFocusable(true);
                             info.addAction(AccessibilityAction.ACTION_FOCUS);
diff --git a/android/TerminalApp/res/values/strings.xml b/android/TerminalApp/res/values/strings.xml
index 884e5f0..20fd95d 100644
--- a/android/TerminalApp/res/values/strings.xml
+++ b/android/TerminalApp/res/values/strings.xml
@@ -27,7 +27,7 @@
     <!-- Description of an empty line in the terminal. This is read by talkback. [CHAR LIMIT=none] -->
     <string name="empty_line">Empty line</string>
     <!-- Description of the hint supported by the terminal UI elements. This is read by talkback. [CHAR LIMIT=none] -->
-    <string name="double_tap_to_edit_text">Double-tap to go to cursor</string>
+    <string name="double_tap_to_edit_text">Double-tap to type input</string>
 
     <!-- Installer activity title [CHAR LIMIT=none] -->
     <string name="installer_title_text">Install Linux terminal</string>
diff --git a/android/virtmgr/src/aidl.rs b/android/virtmgr/src/aidl.rs
index 0f81f3d..e9074c6 100644
--- a/android/virtmgr/src/aidl.rs
+++ b/android/virtmgr/src/aidl.rs
@@ -889,34 +889,18 @@
         .context("Failed to extract vendor hashtree digest")
         .or_service_specific_exception(-1)?;
 
-    let vendor_hashtree_digest = if let Some(ref vendor_hashtree_digest) = vendor_hashtree_digest {
+    let mut trusted_props = 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."
         );
-        Some((cstr!("vendor_hashtree_descriptor_root_digest"), vendor_hashtree_digest.as_slice()))
+        vec![(cstr!("vendor_hashtree_descriptor_root_digest"), vendor_hashtree_digest.as_slice())]
     } else {
-        None
+        vec![]
     };
 
-    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 key_material;
     let mut untrusted_props = Vec::with_capacity(2);
     if cfg!(llpvm_changes) {
         instance_id = extract_instance_id(config);
@@ -925,7 +909,14 @@
         if want_updatable && is_secretkeeper_supported() {
             // Let guest know that it can defer rollback protection to Secretkeeper by setting
             // an empty property in untrusted node in DT. This enables Updatable VMs.
-            untrusted_props.push((cstr!("defer-rollback-protection"), &[]))
+            untrusted_props.push((cstr!("defer-rollback-protection"), &[]));
+            let sk: Strong<dyn ISecretkeeper> =
+                binder::wait_for_interface(SECRETKEEPER_IDENTIFIER)?;
+            if sk.getInterfaceVersion()? >= 2 {
+                let PublicKey { keyMaterial } = sk.getSecretkeeperIdentity()?;
+                key_material = keyMaterial;
+                trusted_props.push((cstr!("secretkeeper_public_key"), key_material.as_slice()));
+            }
         }
     }
 
diff --git a/android/vm_demo_native/main.cpp b/android/vm_demo_native/main.cpp
index d7ff02e..e1acc05 100644
--- a/android/vm_demo_native/main.cpp
+++ b/android/vm_demo_native/main.cpp
@@ -361,8 +361,10 @@
 
 // This is the main routine that follows the steps in order
 Result<void> inner_main() {
-    TemporaryDir work_dir;
-    std::string work_dir_path(work_dir.path);
+    std::string work_dir_path("/data/local/tmp/vm_demo/");
+    if (mkdir(work_dir_path.c_str(), 0700) == -1 && errno != EEXIST) {
+        return ErrnoError() << "failed to create working directory " << work_dir_path.c_str();
+    }
 
     // Step 1: connect to the virtualizationservice
     unique_fd fd = OR_RETURN(get_service_fd());
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 9eb478b..9104adc 100755
--- a/build/debian/build.sh
+++ b/build/debian/build.sh
@@ -70,6 +70,7 @@
 install_prerequisites() {
 	apt update
 	packages=(
+		apt-utils
 		automake
 		binfmt-support
 		build-essential
@@ -186,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
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 f750f62..68b715d 100644
--- a/build/microdroid/Android.bp
+++ b/build/microdroid/Android.bp
@@ -487,7 +487,7 @@
     ],
 }
 
-flag_aware_avb_add_hash_footer_defaults {
+avb_add_hash_footer_defaults {
     name: "microdroid_kernel_signed_defaults",
     src: ":empty_file",
     partition_name: "boot",
@@ -502,10 +502,16 @@
             enabled: true,
         },
     },
+}
+
+MICRODROID_GKI_ROLLBACK_INDEX = 1
+
+flag_aware_avb_add_hash_footer_defaults {
+    name: "microdroid_kernel_cap_defaults",
     // Below are properties that are conditionally set depending on value of build flags.
     soong_config_variables: {
         release_avf_enable_llpvm_changes: {
-            rollback_index: 1,
+            rollback_index: MICRODROID_GKI_ROLLBACK_INDEX,
             props: [
                 {
                     name: "com.android.virt.cap",
@@ -516,9 +522,36 @@
     },
 }
 
+flag_aware_avb_add_hash_footer_defaults {
+    name: "microdroid_kernel_cap_with_uefi_defaults",
+    // Below are properties that are conditionally set depending on value of build flags.
+    soong_config_variables: {
+        release_avf_enable_llpvm_changes: {
+            rollback_index: MICRODROID_GKI_ROLLBACK_INDEX,
+            props: [
+                {
+                    name: "com.android.virt.cap",
+                    value: "secretkeeper_protection|supports_uefi_boot",
+                },
+            ],
+            conditions_default: {
+                props: [
+                    {
+                        name: "com.android.virt.cap",
+                        value: "supports_uefi_boot",
+                    },
+                ],
+            },
+        },
+    },
+}
+
 avb_add_hash_footer {
     name: "microdroid_kernel_signed",
-    defaults: ["microdroid_kernel_signed_defaults"],
+    defaults: [
+        "microdroid_kernel_signed_defaults",
+        "microdroid_kernel_cap_defaults",
+    ],
     filename: "microdroid_kernel",
     arch: {
         arm64: {
@@ -550,7 +583,10 @@
 
 avb_add_hash_footer {
     name: "microdroid_kernel_16k_signed",
-    defaults: ["microdroid_kernel_signed_defaults"],
+    defaults: [
+        "microdroid_kernel_signed_defaults",
+        "microdroid_kernel_cap_defaults",
+    ],
     filename: "microdroid_kernel_16k",
     arch: {
         arm64: {
@@ -590,10 +626,9 @@
     src: "microdroid_gki-android15-6.6.json",
 }
 
-avb_add_hash_footer {
-    name: "microdroid_gki-android15-6.6_kernel_signed",
+avb_add_hash_footer_defaults {
+    name: "microdroid_gki_kernel_signed_defaults",
     defaults: ["microdroid_kernel_signed_defaults"],
-    filename: "microdroid_gki-android15-6.6_kernel_signed",
     arch: {
         arm64: {
             src: ":microdroid_gki_kernel_prebuilts-android15-6.6-arm64",
@@ -608,6 +643,24 @@
     ],
 }
 
+avb_add_hash_footer {
+    name: "microdroid_gki-android15-6.6_kernel_signed",
+    defaults: [
+        "microdroid_gki_kernel_signed_defaults",
+        "microdroid_kernel_cap_defaults",
+    ],
+    filename: "microdroid_gki-android15-6.6_kernel_signed",
+}
+
+avb_add_hash_footer {
+    name: "microdroid_gki-android15-6.6_kernel_signed_supports_uefi_boot",
+    defaults: [
+        "microdroid_gki_kernel_signed_defaults",
+        "microdroid_kernel_cap_with_uefi_defaults",
+    ],
+    filename: "microdroid_gki-android15-6.6_kernel_signed_supports_uefi_boot",
+}
+
 // HACK: use cc_genrule for arch-specific properties
 cc_genrule {
     name: "microdroid_gki-android15-6.6_kernel_signed-lz4",
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/trusty/test_vm/Android.bp b/guest/trusty/test_vm/Android.bp
new file mode 100644
index 0000000..d10bf6e
--- /dev/null
+++ b/guest/trusty/test_vm/Android.bp
@@ -0,0 +1,76 @@
+// Copyright (C) 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.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+    default_team: "trendy_team_trusty",
+}
+
+// python -c "import hashlib; print(hashlib.sha256(b'trusty_test_vm_salt').hexdigest())"
+trusty_test_vm_salt = "5ce3eab1a08540e1334c83f54b8608aa6c23feee6939693cac41441449c5a51f"
+
+TRUSTY_TEST_VM_VERSION = 1
+
+avb_add_hash_footer {
+    name: "trusty_test_vm_signed",
+    filename: "trusty_test_vm_signed",
+    partition_name: "boot",
+    private_key: ":trusty_vm_sign_key",
+    salt: trusty_test_vm_salt,
+    rollback_index: TRUSTY_TEST_VM_VERSION,
+    src: ":empty_file",
+    enabled: false,
+    arch: {
+        x86_64: {
+            src: ":trusty-test-lk.elf",
+            enabled: true,
+        },
+    },
+}
+
+prebuilt_etc {
+    name: "trusty_test_vm_config",
+    enabled: false,
+    arch: {
+        x86_64: {
+            src: "vm_config_lk_x86_64.json",
+            enabled: true,
+        },
+    },
+    filename: "trusty-test_vm-config.json",
+}
+
+sh_test {
+    name: "TrustyTestVM_UnitTests",
+    src: "trusty-ut-ctrl.sh",
+    filename_from_src: true,
+    data: [
+        ":trusty_test_vm_signed",
+        ":trusty_test_vm_config",
+        "trusty-vm-launcher.sh",
+        "trusty-wait-ready.sh",
+    ],
+    // TODO(b/378367793) use the AndroidTest.xml generated from the trusty
+    // test-map for test_vm payload
+    test_config_template: "AndroidTest.xml",
+    test_suites: [
+        "general-tests",
+    ],
+    enabled: false,
+    arch: {
+        x86_64: {
+            enabled: true,
+        },
+    },
+}
diff --git a/guest/trusty/test_vm/AndroidTest.xml b/guest/trusty/test_vm/AndroidTest.xml
new file mode 100644
index 0000000..d8710ab
--- /dev/null
+++ b/guest/trusty/test_vm/AndroidTest.xml
@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+    <configuration description="Runs {MODULE}">
+    <!-- object type="module_controller" class="com.android.tradefed.testtype.suite.module.CommandSuccessModuleController" -->
+        <!--Skip the test when trusty VM is not enabled. -->
+        <!--option name="run-command" value="getprop trusty.test_vm.nonsecure_vm_ready | grep 1" /-->
+    <!--/object-->
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
+    <!-- Target Preparers - Run Shell Commands -->
+    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+        <option name="cleanup" value="true" />
+        <option name="push-file" key="trusty-ut-ctrl.sh" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh" />
+        <option name="push-file" key="trusty-vm-launcher.sh" value="/data/local/tmp/trusty_test_vm/trusty-vm-launcher.sh" />
+        <option name="push-file" key="trusty-wait-ready.sh" value="/data/local/tmp/trusty_test_vm/trusty-wait-ready.sh" />
+        <option name="push-file" key="trusty-test_vm-config.json" value="/data/local/tmp/trusty_test_vm/trusty-test_vm-config.json" />
+        <option name="push-file" key="trusty_test_vm_signed" value="/data/local/tmp/trusty_test_vm/trusty_test_vm_signed" />
+    </target_preparer>
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        <option name="throw-if-cmd-fail" value="true" />
+        <!--Note: the first run-command shall not expect the background command to have started -->
+        <option name="run-bg-command" value="sh /data/local/tmp/trusty_test_vm/trusty-vm-launcher.sh" />
+        <option name="run-command" value="sh /data/local/tmp/trusty_test_vm/trusty-wait-ready.sh" />
+        <option name="run-command" value="start storageproxyd_test_system" />
+        <option name="teardown-command" value="stop storageproxyd_test_system" />
+        <option name="teardown-command" value="killall storageproxyd_test_system || true" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.binary.ExecutableTargetTest" >
+        <option name="parse-gtest" value="true" />
+        <option name="abort-if-device-lost" value="true"/>
+        <option name="abort-if-root-lost" value="true" />
+        <option name="per-binary-timeout" value="10m" />
+        <option name="test-command-line" key="com.android.kernel.mmutest" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.kernel.mmutest"/>
+        <option name="test-command-line" key="com.android.kernel.threadtest" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.kernel.threadtest"/>
+        <option name="test-command-line" key="com.android.kernel.iovectest" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.kernel.iovectest"/>
+        <option name="test-command-line" key="com.android.kernel.timertest" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.kernel.timertest"/>
+        <option name="test-command-line" key="com.android.kernel.btitest" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.kernel.btitest"/>
+        <option name="test-command-line" key="com.android.kernel.cachetest" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.kernel.cachetest"/>
+        <option name="test-command-line" key="com.android.kernel.console-unittest" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.kernel.console-unittest"/>
+        <option name="test-command-line" key="com.android.kernel.dpc-unittest" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.kernel.dpc-unittest"/>
+        <option name="test-command-line" key="com.android.kernel.iovectest" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.kernel.iovectest"/>
+        <option name="test-command-line" key="com.android.kernel.ktipc.test" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.kernel.ktipc.test"/>
+        <option name="test-command-line" key="com.android.kernel.memorytest" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.kernel.memorytest"/>
+        <option name="test-command-line" key="com.android.kernel.pactest" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.kernel.pactest"/>
+        <option name="test-command-line" key="com.android.kernel.uirq-unittest" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.kernel.uirq-unittest"/>
+        <option name="test-command-line" key="com.android.kernel.usercopy-unittest" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.kernel.usercopy-unittest"/>
+        <option name="test-command-line" key="com.android.kernel.userscstest" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.kernel.userscstest"/>
+        <option name="test-command-line" key="com.android.trusty.rust.keymint.test" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.trusty.rust.keymint.test"/>
+        <option name="test-command-line" key="com.android.manifesttest" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.manifesttest"/>
+        <option name="test-command-line" key="com.android.memref.test" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.memref.test"/>
+        <option name="test-command-line" key="com.android.trusty.rust.memref.test" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.trusty.rust.memref.test"/>
+        <option name="test-command-line" key="com.android.timer-unittest" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.timer-unittest"/>
+        <option name="test-command-line" key="com.android.ipc-unittest.ctrl" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.ipc-unittest.ctrl"/>
+        <!--option name="test-command-line" key="com.android.trusty.cfitest" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.trusty.cfitest"/-->
+        <option name="test-command-line" key="com.android.trusty.crashtest" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.trusty.crashtest"/>
+        <option name="test-command-line" key="com.android.trusty.dlmalloctest" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.trusty.dlmalloctest"/>
+        <option name="test-command-line" key="com.android.trusty.hwaes.test" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.trusty.hwaes.test"/>
+        <option name="test-command-line" key="com.android.trusty.hwbcc.test" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.trusty.hwbcc.test"/>
+        <option name="test-command-line" key="com.android.trusty.rust.tipc.test" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.trusty.rust.tipc.test"/>
+        <option name="test-command-line" key="com.android.trusty.rust.hwkey.test" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.trusty.rust.hwkey.test"/>
+        <option name="test-command-line" key="com.android.trusty.rust.hwbcc.test" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.trusty.rust.hwbcc.test"/>
+        <option name="test-command-line" key="com.android.trusty.rust.hwwsk.test" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.trusty.rust.hwwsk.test"/>
+        <option name="test-command-line" key="com.android.trusty.rust.storage.test" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.trusty.rust.storage.test"/>
+        <option name="test-command-line" key="com.android.trusty.smc.test" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.trusty.smc.test"/>
+        <option name="test-command-line" key="com.android.uirq-unittest" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.uirq-unittest"/>
+        <!-- Unit tests for legacy hwcrypto services - these hwcrypto services are used by hwcryptohal /-->
+        <option name="test-command-line" key="com.android.trusty.hwcrypto.test" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.trusty.hwcrypto.test"/>
+        <option name="test-command-line" key="com.android.trusty.hwrng.test" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.trusty.hwrng.test"/>
+        <!-- Unit tests for hwcryptohal (exposing IHWCryptoKey/IHWCryptoOperations AIDL) - Note: VTS tests are defined alongside the interface /-->
+        <option name="test-command-line" key="com.android.trusty.rust.hwcryptohalserver.test" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.trusty.rust.hwcryptohalserver.test"/>
+        <option name="test-command-line" key="com.android.trusty.rust.hwcryptohal_common.test" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.trusty.rust.hwcryptohal_common.test"/>
+        <option name="test-command-line" key="com.android.trusty.rust.hwcryptokey_test.test" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.trusty.rust.hwcryptokey_test.test"/>
+    </test>
+    <!-- disabling storage test as they are redundant with the VTS -->
+    <!--test class="com.android.tradefed.testtype.binary.ExecutableTargetTest" >
+        <option name="parse-gtest" value="true" />
+        <option name="abort-if-device-lost" value="true" />
+        <option name="abort-if-root-lost" value="true" />
+        <option name="per-binary-timeout" value="40m" />
+        <option name="test-command-line" key="com.android.trusty.rust.storage_unittest_aidl.test" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.trusty.rust.storage_unittest_aidl.test"/>
+        <option name="test-command-line" key="com.android.trusty.rust.storage_unittest_aidl_ns.test" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.trusty.rust.storage_unittest_aidl_ns.test"/>
+        <option name="test-command-line" key="com.android.storage-unittest.tp" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.storage-unittest.tp"/>
+        <option name="test-command-line" key="com.android.storage-unittest.tdea" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.storage-unittest.tdea"/>
+        <option name="test-command-line" key="com.android.storage-unittest.nsp" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.storage-unittest.nsp"/>
+        <option name="test-command-line" key="com.android.storage-unittest.td" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.storage-unittest.td"/>
+        <option name="test-command-line" key="com.android.storage-unittest.tdp" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.storage-unittest.tdp"/>
+    </test-->
+    <test class="com.android.tradefed.testtype.binary.ExecutableTargetTest" >
+        <option name="parse-gtest" value="true" />
+        <!--option name="abort-if-device-lost" value="true" /-->
+        <!--option name="abort-if-root-lost" value="true" /-->
+        <option name="per-binary-timeout" value="40m" />
+        <option name="test-command-line" key="com.android.trusty.rust.binder_rpc_test.test" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.trusty.rust.binder_rpc_test.test"/>
+        <option name="test-command-line" key="com.android.trusty.binder.test" value="/data/local/tmp/trusty_test_vm/trusty-ut-ctrl.sh com.android.trusty.binder.test"/>
+    </test>
+    </configuration>
diff --git a/guest/trusty/test_vm/README.md b/guest/trusty/test_vm/README.md
new file mode 100644
index 0000000..1673844
--- /dev/null
+++ b/guest/trusty/test_vm/README.md
@@ -0,0 +1,7 @@
+## Trusty test_vm
+
+The Trusty test_vm ought to include the test TAs for different test types:
+- Trusty kernel OS test
+- Trusty IPC tests
+- Trusty user-space tests for service TAs (DT tree for example)
+- and most importantly the VTS tests TA for the trusted HALs.
diff --git a/guest/trusty/test_vm/TEST_MAPPING b/guest/trusty/test_vm/TEST_MAPPING
new file mode 100644
index 0000000..6f2b56e
--- /dev/null
+++ b/guest/trusty/test_vm/TEST_MAPPING
@@ -0,0 +1,9 @@
+{
+  "trusty-test_vm-presubmit": [
+  ],
+  "trusty-test_vm-postsubmit": [
+    {
+        "name": "TrustyTestVM_UnitTests"
+    }
+  ]
+}
diff --git a/guest/trusty/test_vm/trusty-ut-ctrl.sh b/guest/trusty/test_vm/trusty-ut-ctrl.sh
new file mode 100644
index 0000000..77a9459
--- /dev/null
+++ b/guest/trusty/test_vm/trusty-ut-ctrl.sh
@@ -0,0 +1,17 @@
+#!/bin/sh
+#
+# Copyright (C) 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.
+
+/system_ext/bin/trusty-ut-ctrl.system -D VSOCK:${2:-$(getprop trusty.test_vm.vm_cid)}:1 $1
diff --git a/guest/trusty/test_vm/trusty-vm-launcher.sh b/guest/trusty/test_vm/trusty-vm-launcher.sh
new file mode 100755
index 0000000..cb8661f
--- /dev/null
+++ b/guest/trusty/test_vm/trusty-vm-launcher.sh
@@ -0,0 +1,17 @@
+#!/bin/sh
+
+# Copyright 2024 Google Inc. All rights reserved.
+#
+# 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.
+
+/apex/com.android.virt/bin/vm run /data/local/tmp/trusty_test_vm/trusty-test_vm-config.json
diff --git a/guest/trusty/test_vm/trusty-wait-ready.sh b/guest/trusty/test_vm/trusty-wait-ready.sh
new file mode 100755
index 0000000..842853c
--- /dev/null
+++ b/guest/trusty/test_vm/trusty-wait-ready.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+set -euo pipefail
+
+function get_cid {
+    local max_cid
+    max_cid=$(/apex/com.android.virt/bin/vm list | awk 'BEGIN { FS="[:,]" } /cid/ { print $2; }' | sort -n | tail -1)
+
+    # return the value trimmed from whitespaces
+    echo "${max_cid}" | xargs
+}
+
+function wait_for_cid {
+    TIMES=${1:-20}
+    X=0
+    local init_cid
+    init_cid=$(get_cid)
+    while [ "$TIMES" -eq 0 ] || [ "$TIMES" -gt "$X" ]
+    do
+      local cid
+      cid=$(get_cid)
+      echo "wait_for_cid: retry $(( X++ )) / $TIMES : init_cid=$init_cid cid=$cid";
+      if [ "$cid" -gt "$init_cid" ]
+      then
+        break
+      else
+        sleep 2
+      fi
+    done
+    setprop trusty.test_vm.vm_cid "$cid"
+}
+
+# This script is expected to be started before the trusty_test_vm is started
+# wait_for_cid gets the max cid and wait for it to be updated as an indication
+# that the trusty_test_vm has properly started.
+# wait_for_cid polls for the CID change at 2 seconds intervals
+# the input argument is the max number of retries (20 by default)
+wait_for_cid "$@"
+
+echo trusty.test_vm.vm_cid="$(getprop trusty.test_vm.vm_cid)"
diff --git a/guest/trusty/test_vm/vm_config_lk_x86_64.json b/guest/trusty/test_vm/vm_config_lk_x86_64.json
new file mode 100644
index 0000000..5effca5
--- /dev/null
+++ b/guest/trusty/test_vm/vm_config_lk_x86_64.json
@@ -0,0 +1,6 @@
+{
+    "name": "trusty_test_vm",
+    "kernel": "/data/local/tmp/trusty_test_vm/trusty_test_vm_signed",
+    "platform_version": "1.0",
+    "memory_mib": 112
+}
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/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/bionic.rs b/libs/libvmbase/src/bionic.rs
index 8b40dae..3c0cd6f 100644
--- a/libs/libvmbase/src/bionic.rs
+++ b/libs/libvmbase/src/bionic.rs
@@ -72,6 +72,7 @@
 pub static mut ERRNO: c_int = 0;
 
 #[no_mangle]
+#[allow(unused_unsafe)]
 unsafe extern "C" fn __errno() -> *mut c_int {
     // SAFETY: C functions which call this are only called from the main thread, not from exception
     // handlers.
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..cf3a8fc 100644
--- a/libs/libvmbase/src/layout.rs
+++ b/libs/libvmbase/src/layout.rs
@@ -14,10 +14,12 @@
 
 //! Memory layout.
 
+#![allow(unused_unsafe)]
+
 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 +93,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