Merge "Add some unit tests"
diff --git a/apex/Android.bp b/apex/Android.bp
index 4e64e50..596493a 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -104,6 +104,9 @@
     host_required: [
         "vm_shell",
     ],
+    apps: [
+        "EmptyPayloadApp",
+    ],
 }
 
 apex_defaults {
diff --git a/apex/empty-payload-apk/Android.bp b/apex/empty-payload-apk/Android.bp
new file mode 100644
index 0000000..70e6754
--- /dev/null
+++ b/apex/empty-payload-apk/Android.bp
@@ -0,0 +1,26 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app {
+    name: "EmptyPayloadApp",
+    installable: true,
+    jni_libs: ["MicrodroidEmptyPayloadJniLib"],
+    apex_available: ["com.android.virt"],
+    sdk_version: "system_current",
+    jni_uses_platform_apis: true,
+    min_sdk_version: "UpsideDownCake",
+    target_sdk_version: "UpsideDownCake",
+    compile_multilib: "first",
+    stl: "none",
+}
+
+cc_library {
+    name: "MicrodroidEmptyPayloadJniLib",
+    srcs: ["empty_binary.cpp"],
+    shared_libs: ["libvm_payload#current"],
+    installable: true,
+    apex_available: ["com.android.virt"],
+    compile_multilib: "first",
+    stl: "none",
+}
diff --git a/apex/empty-payload-apk/AndroidManifest.xml b/apex/empty-payload-apk/AndroidManifest.xml
new file mode 100644
index 0000000..e649744
--- /dev/null
+++ b/apex/empty-payload-apk/AndroidManifest.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.microdroid.empty_payload">
+
+    <uses-permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE" />
+    <uses-feature android:name="android.software.virtualization_framework" android:required="true" />
+    <application android:testOnly="true" android:hasCode="false" />
+
+</manifest>
diff --git a/apex/empty-payload-apk/empty_binary.cpp b/apex/empty-payload-apk/empty_binary.cpp
new file mode 100644
index 0000000..4308954
--- /dev/null
+++ b/apex/empty-payload-apk/empty_binary.cpp
@@ -0,0 +1,22 @@
+#include <stdio.h>
+#include <unistd.h>
+#include <vm_main.h>
+#include <vm_payload.h>
+
+extern "C" int AVmPayload_main() {
+    // disable buffering to communicate seamlessly
+    setvbuf(stdin, nullptr, _IONBF, 0);
+    setvbuf(stdout, nullptr, _IONBF, 0);
+    setvbuf(stderr, nullptr, _IONBF, 0);
+
+    printf("Hello Microdroid\n");
+
+    AVmPayload_notifyPayloadReady();
+
+    // Wait forever to allow developer to interact with Microdroid shell
+    for (;;) {
+        pause();
+    }
+
+    return 0;
+}
diff --git a/docs/getting_started/index.md b/docs/getting_started/index.md
index 5f552f9..a15c4c7 100644
--- a/docs/getting_started/index.md
+++ b/docs/getting_started/index.md
@@ -123,7 +123,16 @@
 ## Spawning your own VMs with Microdroid
 
 [Microdroid](../../microdroid/README.md) is a lightweight version of Android that is intended to run
-on pVM. You can manually run the demo app on top of Microdroid as follows:
+on pVM. You can run a Microdroid with empty payload using the following command:
+
+```shell
+adb shell /apex/com.android.virt/bin/vm run-microdroid --debug full
+```
+
+The `instance.img` and `apk.idsig` files will be stored in a subdirectory under
+`/data/local/tmp/microdroid`, that `vm` will create.
+
+Atlernatively, you can manually run the demo app on top of Microdroid as follows:
 
 ```shell
 UNBUNDLED_BUILD_SDKS_FROM_SOURCE=true TARGET_BUILD_APPS=MicrodroidDemoApp m apps_only dist
diff --git a/encryptedstore/src/main.rs b/encryptedstore/src/main.rs
index d7d2382..9c8311d 100644
--- a/encryptedstore/src/main.rs
+++ b/encryptedstore/src/main.rs
@@ -20,7 +20,8 @@
 
 use anyhow::{ensure, Context, Result};
 use clap::{arg, App};
-use dm::{crypt::CipherType, util};
+use dm::crypt::CipherType;
+use dm::util;
 use log::info;
 use std::ffi::CString;
 use std::fs::{create_dir_all, OpenOptions};
@@ -45,7 +46,7 @@
         .args(&[
             arg!(--blkdevice <FILE> "the block device backing the encrypted storage")
                 .required(true),
-            arg!(--key <KEY> "key (in hex) equivalent to 64 bytes)").required(true),
+            arg!(--key <KEY> "key (in hex) equivalent to 32 bytes)").required(true),
             arg!(--mountpoint <MOUNTPOINT> "mount point for the storage").required(true),
         ])
         .get_matches();
@@ -87,12 +88,11 @@
 fn enable_crypt(data_device: &Path, key: &str, name: &str) -> Result<PathBuf> {
     let dev_size = util::blkgetsize64(data_device)?;
     let key = hex::decode(key).context("Unable to decode hex key")?;
-    ensure!(key.len() == 64, "We need 64 bytes' key for aes-xts cipher for block encryption");
 
     // Create the dm-crypt spec
     let target = dm::crypt::DmCryptTargetBuilder::default()
         .data_device(data_device, dev_size)
-        .cipher(CipherType::AES256XTS) // TODO(b/259253336) Move to HCTR2 based encryption.
+        .cipher(CipherType::AES256HCTR2)
         .key(&key)
         .build()
         .context("Couldn't build the DMCrypt target")?;
diff --git a/libs/devicemapper/Android.bp b/libs/devicemapper/Android.bp
index 783fa79..9fa010c 100644
--- a/libs/devicemapper/Android.bp
+++ b/libs/devicemapper/Android.bp
@@ -13,6 +13,7 @@
         "libbitflags",
         "liblibc",
         "libdata_model",
+        "libhex",
         "libnix",
         "libuuid",
     ],
@@ -33,6 +34,7 @@
     defaults: ["libdm_rust.defaults"],
     test_suites: ["general-tests"],
     rustlibs: [
+        "librustutils",
         "libscopeguard",
         "libtempfile",
     ],
diff --git a/libs/devicemapper/src/crypt.rs b/libs/devicemapper/src/crypt.rs
index 9b715a5..b2e677a 100644
--- a/libs/devicemapper/src/crypt.rs
+++ b/libs/devicemapper/src/crypt.rs
@@ -17,10 +17,9 @@
 /// `crypt` module implements the "crypt" target in the device mapper framework. Specifically,
 /// it provides `DmCryptTargetBuilder` struct which is used to construct a `DmCryptTarget` struct
 /// which is then given to `DeviceMapper` to create a mapper device.
-use crate::util::*;
 use crate::DmTargetSpec;
 
-use anyhow::{bail, Context, Result};
+use anyhow::{ensure, Context, Result};
 use data_model::DataInit;
 use std::io::Write;
 use std::mem::size_of;
@@ -32,10 +31,34 @@
 // Documentation/admin-guide/device-mapper/dm-crypt.rst
 
 /// Supported ciphers
+#[derive(Clone, Copy, Debug)]
 pub enum CipherType {
-    // TODO(b/253394457) Include ciphers with authenticated modes as well
+    // AES-256-HCTR2 takes a 32-byte key
+    AES256HCTR2,
+    // XTS requires key of twice the length of the underlying block cipher i.e., 64B for AES256
     AES256XTS,
 }
+impl CipherType {
+    fn get_kernel_crypto_name(&self) -> &str {
+        match *self {
+            // We use "plain64" as the IV/nonce generation algorithm -
+            // which basically is the sector number.
+            CipherType::AES256HCTR2 => "aes-hctr2-plain64",
+            CipherType::AES256XTS => "aes-xts-plain64",
+        }
+    }
+
+    fn get_required_key_size(&self) -> usize {
+        match *self {
+            CipherType::AES256HCTR2 => 32,
+            CipherType::AES256XTS => 64,
+        }
+    }
+
+    fn validata_key_size(&self, key_size: usize) -> bool {
+        key_size == self.get_required_key_size()
+    }
+}
 
 pub struct DmCryptTarget(Box<[u8]>);
 
@@ -59,7 +82,7 @@
 impl<'a> Default for DmCryptTargetBuilder<'a> {
     fn default() -> Self {
         DmCryptTargetBuilder {
-            cipher: CipherType::AES256XTS,
+            cipher: CipherType::AES256HCTR2,
             key: None,
             iv_offset: 0,
             device_path: None,
@@ -112,8 +135,13 @@
             .to_str()
             .context("data device path is not encoded in utf8")?;
 
-        let key =
-            if let Some(key) = self.key { hexstring_from(key) } else { bail!("key is not set") };
+        ensure!(self.key.is_some(), "key is not set");
+        // Unwrap is safe because we already made sure key.is_some()
+        ensure!(
+            self.cipher.validata_key_size(self.key.unwrap().len()),
+            format!("Invalid key size for cipher:{}", self.cipher.get_kernel_crypto_name())
+        );
+        let key = hex::encode(self.key.unwrap());
 
         // Step2: serialize the information according to the spec, which is ...
         // DmTargetSpec{...}
@@ -121,7 +149,7 @@
         // <offset> [<#opt_params> <opt_params>]
         let mut body = String::new();
         use std::fmt::Write;
-        write!(&mut body, "{} ", get_kernel_crypto_name(&self.cipher))?;
+        write!(&mut body, "{} ", self.cipher.get_kernel_crypto_name())?;
         write!(&mut body, "{} ", key)?;
         write!(&mut body, "{} ", self.iv_offset)?;
         write!(&mut body, "{} ", device_path)?;
@@ -145,9 +173,3 @@
         Ok(DmCryptTarget(buf.into_boxed_slice()))
     }
 }
-
-fn get_kernel_crypto_name(cipher: &CipherType) -> &str {
-    match cipher {
-        CipherType::AES256XTS => "aes-xts-plain64",
-    }
-}
diff --git a/libs/devicemapper/src/lib.rs b/libs/devicemapper/src/lib.rs
index ebe71e4..9069eb2 100644
--- a/libs/devicemapper/src/lib.rs
+++ b/libs/devicemapper/src/lib.rs
@@ -234,12 +234,28 @@
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crypt::DmCryptTargetBuilder;
+    use crypt::{CipherType, DmCryptTargetBuilder};
+    use rustutils::system_properties;
     use std::fs::{read, File, OpenOptions};
     use std::io::Write;
 
-    const KEY: &[u8; 32] = b"thirtytwobyteslongreallylongword";
-    const DIFFERENT_KEY: &[u8; 32] = b"drowgnolyllaergnolsetybowtytriht";
+    // Just a logical set of keys to make testing easy. This has no real meaning.
+    struct KeySet<'a> {
+        cipher: CipherType,
+        key: &'a [u8],
+        different_key: &'a [u8],
+    }
+
+    const KEY_SET_XTS: KeySet = KeySet {
+        cipher: CipherType::AES256XTS,
+        key: b"sixtyfourbyteslongsentencearerarebutletsgiveitatrycantbethathard",
+        different_key: b"drahtahtebtnacyrtatievigsteltuberareraecnetnesgnolsetybruofytxis",
+    };
+    const KEY_SET_HCTR2: KeySet = KeySet {
+        cipher: CipherType::AES256HCTR2,
+        key: b"thirtytwobyteslongreallylongword",
+        different_key: b"drowgnolyllaergnolsetybowtytriht",
+    };
 
     // Create a file in given temp directory with given size
     fn prepare_tmpfile(test_dir: &Path, filename: &str, sz: u64) -> PathBuf {
@@ -254,14 +270,54 @@
         f.write_all(data).unwrap();
     }
 
+    // TODO(b/250880499): delete_device() doesn't really delete it even without DM_DEFERRED_REMOVE.
+    // Hence, we have to create a new device with a different name for each test. Retrying
+    // the test on same machine without reboot will also fail.
     fn delete_device(dm: &DeviceMapper, name: &str) -> Result<()> {
         dm.delete_device_deferred(name)?;
         wait_for_path_disappears(Path::new(MAPPER_DEV_ROOT).join(name))?;
         Ok(())
     }
 
+    // TODO(b/260692911): Find a better way to skip a test instead of silently passing it.
+    fn is_hctr2_supported() -> bool {
+        // hctr2 is NOT enabled in kernel 5.10 or lower. We run Microdroid tests on kernel versions
+        // 5.10 or above & therefore,  we don't really care to skip test on other versions.
+        if let Some(version) = system_properties::read("ro.kernel.version")
+            .expect("Unable to read system property ro.kernel.version")
+        {
+            version != "5.10"
+        } else {
+            panic!("Could not read property: kernel.version!!");
+        }
+    }
+
     #[test]
-    fn mapping_again_keeps_data() {
+    fn mapping_again_keeps_data_xts() {
+        mapping_again_keeps_data(&KEY_SET_XTS, "name1");
+    }
+
+    #[test]
+    fn mapping_again_keeps_data_hctr2() {
+        if !is_hctr2_supported() {
+            return;
+        }
+        mapping_again_keeps_data(&KEY_SET_HCTR2, "name2");
+    }
+    #[test]
+    fn data_inaccessible_with_diff_key_xts() {
+        data_inaccessible_with_diff_key(&KEY_SET_XTS, "name3");
+    }
+
+    #[test]
+    fn data_inaccessible_with_diff_key_hctr2() {
+        if !is_hctr2_supported() {
+            return;
+        }
+        data_inaccessible_with_diff_key(&KEY_SET_HCTR2, "name4");
+    }
+
+    fn mapping_again_keeps_data(keyset: &KeySet, device: &str) {
         // This test creates 2 different crypt devices using same key backed by same data_device
         // -> Write data on dev1 -> Check the data is visible & same on dev2
         let dm = DeviceMapper::new().unwrap();
@@ -278,28 +334,33 @@
             /*writable*/ true,
         )
         .unwrap();
+        let device_diff = device.to_owned() + "_diff";
+
         scopeguard::defer! {
             loopdevice::detach(&data_device).unwrap();
-            _ = delete_device(&dm, "crypt1");
-            _ = delete_device(&dm, "crypt2");
+            _ = delete_device(&dm, device);
+            _ = delete_device(&dm, &device_diff);
         }
 
-        let target =
-            DmCryptTargetBuilder::default().data_device(&data_device, sz).key(KEY).build().unwrap();
+        let target = DmCryptTargetBuilder::default()
+            .data_device(&data_device, sz)
+            .cipher(keyset.cipher)
+            .key(keyset.key)
+            .build()
+            .unwrap();
 
-        let mut crypt_device = dm.create_crypt_device("crypt1", &target).unwrap();
+        let mut crypt_device = dm.create_crypt_device(device, &target).unwrap();
         write_to_dev(&crypt_device, inputimg);
 
         // Recreate another device using same target spec & check if the content is the same
-        crypt_device = dm.create_crypt_device("crypt2", &target).unwrap();
+        crypt_device = dm.create_crypt_device(&device_diff, &target).unwrap();
 
         let crypt = read(crypt_device).unwrap();
         assert_eq!(inputimg.len(), crypt.len()); // fail early if the size doesn't match
         assert_eq!(inputimg, crypt.as_slice());
     }
 
-    #[test]
-    fn data_inaccessible_with_diff_key() {
+    fn data_inaccessible_with_diff_key(keyset: &KeySet, device: &str) {
         // This test creates 2 different crypt devices using different keys backed
         // by same data_device -> Write data on dev1 -> Check the data is visible but not the same on dev2
         let dm = DeviceMapper::new().unwrap();
@@ -316,26 +377,32 @@
             /*writable*/ true,
         )
         .unwrap();
+        let device_diff = device.to_owned() + "_diff";
         scopeguard::defer! {
             loopdevice::detach(&data_device).unwrap();
-            _ = delete_device(&dm, "crypt3");
-            _ = delete_device(&dm, "crypt4");
+            _ = delete_device(&dm, device);
+            _ = delete_device(&dm, &device_diff);
         }
 
-        let target =
-            DmCryptTargetBuilder::default().data_device(&data_device, sz).key(KEY).build().unwrap();
+        let target = DmCryptTargetBuilder::default()
+            .data_device(&data_device, sz)
+            .cipher(keyset.cipher)
+            .key(keyset.key)
+            .build()
+            .unwrap();
         let target2 = DmCryptTargetBuilder::default()
             .data_device(&data_device, sz)
-            .key(DIFFERENT_KEY)
+            .cipher(keyset.cipher)
+            .key(keyset.different_key)
             .build()
             .unwrap();
 
-        let mut crypt_device = dm.create_crypt_device("crypt3", &target).unwrap();
+        let mut crypt_device = dm.create_crypt_device(device, &target).unwrap();
 
         write_to_dev(&crypt_device, inputimg);
 
         // Recreate the crypt device again diff key & check if the content is changed
-        crypt_device = dm.create_crypt_device("crypt4", &target2).unwrap();
+        crypt_device = dm.create_crypt_device(&device_diff, &target2).unwrap();
         let crypt = read(crypt_device).unwrap();
         assert_ne!(inputimg, crypt.as_slice());
     }
diff --git a/libs/libfdt/src/lib.rs b/libs/libfdt/src/lib.rs
index 01f7b36..ff1db63 100644
--- a/libs/libfdt/src/lib.rs
+++ b/libs/libfdt/src/lib.rs
@@ -16,6 +16,7 @@
 //! to a bare-metal environment.
 
 #![no_std]
+#![feature(let_else)] // Stabilized in 1.65.0
 
 use core::ffi::{c_int, c_void, CStr};
 use core::fmt;
@@ -136,6 +137,14 @@
     }
 }
 
+fn fdt_err_or_option(val: c_int) -> Result<Option<c_int>> {
+    match fdt_err(val) {
+        Ok(val) => Ok(Some(val)),
+        Err(FdtError::NotFound) => Ok(None),
+        Err(e) => Err(e),
+    }
+}
+
 /// Value of a #address-cells property.
 #[derive(Copy, Clone, Debug)]
 enum AddrCells {
@@ -251,8 +260,8 @@
 }
 
 impl<'a> MemRegIterator<'a> {
-    fn new(reg: RegIterator<'a>) -> Result<Self> {
-        Ok(Self { reg })
+    fn new(reg: RegIterator<'a>) -> Self {
+        Self { reg }
     }
 }
 
@@ -285,45 +294,67 @@
     }
 
     /// Retrieve the standard (deprecated) device_type <string> property.
-    pub fn device_type(&self) -> Result<&CStr> {
+    pub fn device_type(&self) -> Result<Option<&CStr>> {
         self.getprop_str(CStr::from_bytes_with_nul(b"device_type\0").unwrap())
     }
 
     /// Retrieve the standard reg <prop-encoded-array> property.
-    pub fn reg(&self) -> Result<RegIterator<'a>> {
-        let parent = self.parent()?;
+    pub fn reg(&self) -> Result<Option<RegIterator<'a>>> {
+        let reg = CStr::from_bytes_with_nul(b"reg\0").unwrap();
 
-        let addr_cells = parent.address_cells()?;
-        let size_cells = parent.size_cells()?;
-        let cells = self.getprop_cells(CStr::from_bytes_with_nul(b"reg\0").unwrap())?;
+        if let Some(cells) = self.getprop_cells(reg)? {
+            let parent = self.parent()?;
 
-        Ok(RegIterator::new(cells, addr_cells, size_cells))
+            let addr_cells = parent.address_cells()?;
+            let size_cells = parent.size_cells()?;
+
+            Ok(Some(RegIterator::new(cells, addr_cells, size_cells)))
+        } else {
+            Ok(None)
+        }
     }
 
     /// Retrieve the value of a given <string> property.
-    pub fn getprop_str(&self, name: &CStr) -> Result<&CStr> {
-        CStr::from_bytes_with_nul(self.getprop(name)?).map_err(|_| FdtError::BadValue)
+    pub fn getprop_str(&self, name: &CStr) -> Result<Option<&CStr>> {
+        let value = if let Some(bytes) = self.getprop(name)? {
+            Some(CStr::from_bytes_with_nul(bytes).map_err(|_| FdtError::BadValue)?)
+        } else {
+            None
+        };
+        Ok(value)
     }
 
     /// Retrieve the value of a given property as an array of cells.
-    pub fn getprop_cells(&self, name: &CStr) -> Result<CellIterator<'a>> {
-        Ok(CellIterator::new(self.getprop(name)?))
+    pub fn getprop_cells(&self, name: &CStr) -> Result<Option<CellIterator<'a>>> {
+        if let Some(cells) = self.getprop(name)? {
+            Ok(Some(CellIterator::new(cells)))
+        } else {
+            Ok(None)
+        }
     }
 
     /// Retrieve the value of a given <u32> property.
-    pub fn getprop_u32(&self, name: &CStr) -> Result<u32> {
-        let prop = self.getprop(name)?.try_into().map_err(|_| FdtError::BadValue)?;
-        Ok(u32::from_be_bytes(prop))
+    pub fn getprop_u32(&self, name: &CStr) -> Result<Option<u32>> {
+        let value = if let Some(bytes) = self.getprop(name)? {
+            Some(u32::from_be_bytes(bytes.try_into().map_err(|_| FdtError::BadValue)?))
+        } else {
+            None
+        };
+        Ok(value)
     }
 
     /// Retrieve the value of a given <u64> property.
-    pub fn getprop_u64(&self, name: &CStr) -> Result<u64> {
-        let prop = self.getprop(name)?.try_into().map_err(|_| FdtError::BadValue)?;
-        Ok(u64::from_be_bytes(prop))
+    pub fn getprop_u64(&self, name: &CStr) -> Result<Option<u64>> {
+        let value = if let Some(bytes) = self.getprop(name)? {
+            Some(u64::from_be_bytes(bytes.try_into().map_err(|_| FdtError::BadValue)?))
+        } else {
+            None
+        };
+        Ok(value)
     }
 
     /// Retrieve the value of a given property.
-    pub fn getprop(&self, name: &CStr) -> Result<&'a [u8]> {
+    pub fn getprop(&self, name: &CStr) -> Result<Option<&'a [u8]>> {
         let mut len: i32 = 0;
         // SAFETY - Accesses are constrained to the DT totalsize (validated by ctor) and the
         // function respects the passed number of characters.
@@ -337,14 +368,21 @@
                 &mut len as *mut i32,
             )
         } as *const u8;
+
+        let Some(len) = fdt_err_or_option(len)? else {
+            return Ok(None); // Property was not found.
+        };
+        let len = usize::try_from(len).map_err(|_| FdtError::Internal)?;
+
         if prop.is_null() {
-            return fdt_err(len).and(Err(FdtError::Internal));
+            // We expected an error code in len but still received a valid value?!
+            return Err(FdtError::Internal);
         }
-        let len = usize::try_from(fdt_err(len)?).map_err(|_| FdtError::Internal)?;
-        let base =
+
+        let offset =
             (prop as usize).checked_sub(self.fdt.as_ptr() as usize).ok_or(FdtError::Internal)?;
 
-        self.fdt.bytes.get(base..(base + len)).ok_or(FdtError::Internal)
+        Ok(Some(self.fdt.buffer.get(offset..(offset + len)).ok_or(FdtError::Internal)?))
     }
 
     /// Get reference to the containing device tree.
@@ -362,11 +400,7 @@
             )
         };
 
-        match fdt_err(ret) {
-            Ok(offset) => Ok(Some(Self { fdt: self.fdt, offset })),
-            Err(FdtError::NotFound) => Ok(None),
-            Err(e) => Err(e),
-        }
+        Ok(fdt_err_or_option(ret)?.map(|offset| Self { fdt: self.fdt, offset }))
     }
 
     fn address_cells(&self) -> Result<AddrCells> {
@@ -384,6 +418,69 @@
     }
 }
 
+/// Mutable FDT node.
+pub struct FdtNodeMut<'a> {
+    fdt: &'a mut Fdt,
+    offset: c_int,
+}
+
+impl<'a> FdtNodeMut<'a> {
+    /// Append a property name-value (possibly empty) pair to the given node.
+    pub fn appendprop<T: AsRef<[u8]>>(&mut self, name: &CStr, value: &T) -> Result<()> {
+        // SAFETY - Accesses are constrained to the DT totalsize (validated by ctor).
+        let ret = unsafe {
+            libfdt_bindgen::fdt_appendprop(
+                self.fdt.as_mut_ptr(),
+                self.offset,
+                name.as_ptr(),
+                value.as_ref().as_ptr().cast::<c_void>(),
+                value.as_ref().len().try_into().map_err(|_| FdtError::BadValue)?,
+            )
+        };
+
+        fdt_err_expect_zero(ret)
+    }
+
+    /// Append a (address, size) pair property to the given node.
+    pub fn appendprop_addrrange(&mut self, name: &CStr, addr: u64, size: u64) -> Result<()> {
+        // SAFETY - Accesses are constrained to the DT totalsize (validated by ctor).
+        let ret = unsafe {
+            libfdt_bindgen::fdt_appendprop_addrrange(
+                self.fdt.as_mut_ptr(),
+                self.parent()?.offset,
+                self.offset,
+                name.as_ptr(),
+                addr,
+                size,
+            )
+        };
+
+        fdt_err_expect_zero(ret)
+    }
+
+    /// Get reference to the containing device tree.
+    pub fn fdt(&mut self) -> &mut Fdt {
+        self.fdt
+    }
+
+    /// Add a new subnode to the given node and return it as a FdtNodeMut on success.
+    pub fn add_subnode(&'a mut self, name: &CStr) -> Result<Self> {
+        // SAFETY - Accesses are constrained to the DT totalsize (validated by ctor).
+        let ret = unsafe {
+            libfdt_bindgen::fdt_add_subnode(self.fdt.as_mut_ptr(), self.offset, name.as_ptr())
+        };
+
+        Ok(Self { fdt: self.fdt, offset: fdt_err(ret)? })
+    }
+
+    fn parent(&'a self) -> Result<FdtNode<'a>> {
+        // SAFETY - Accesses (read-only) are constrained to the DT totalsize.
+        let ret = unsafe { libfdt_bindgen::fdt_parent_offset(self.fdt.as_ptr(), self.offset) };
+
+        Ok(FdtNode { fdt: &*self.fdt, offset: fdt_err(ret)? })
+    }
+}
+
 /// Iterator over nodes sharing a same compatible string.
 pub struct CompatibleIterator<'a> {
     node: FdtNode<'a>,
@@ -411,10 +508,10 @@
     }
 }
 
-/// Wrapper around low-level read-only libfdt functions.
+/// Wrapper around low-level libfdt functions.
 #[repr(transparent)]
 pub struct Fdt {
-    bytes: [u8],
+    buffer: [u8],
 }
 
 impl Fdt {
@@ -428,6 +525,16 @@
         Ok(fdt)
     }
 
+    /// Wraps a mutable slice containing a Flattened Device Tree.
+    ///
+    /// Fails if the FDT does not pass validation.
+    pub fn from_mut_slice(fdt: &mut [u8]) -> Result<&mut Self> {
+        // SAFETY - The FDT will be validated before it is returned.
+        let fdt = unsafe { Self::unchecked_from_mut_slice(fdt) };
+        fdt.check_full()?;
+        Ok(fdt)
+    }
+
     /// Wraps a slice containing a Flattened Device Tree.
     ///
     /// # Safety
@@ -437,35 +544,71 @@
         mem::transmute::<&[u8], &Self>(fdt)
     }
 
+    /// Wraps a mutable slice containing a Flattened Device Tree.
+    ///
+    /// # Safety
+    ///
+    /// The returned FDT might be invalid, only use on slices containing a valid DT.
+    pub unsafe fn unchecked_from_mut_slice(fdt: &mut [u8]) -> &mut Self {
+        mem::transmute::<&mut [u8], &mut Self>(fdt)
+    }
+
+    /// Make the whole slice containing the DT available to libfdt.
+    pub fn unpack(&mut self) -> Result<()> {
+        // SAFETY - "Opens" the DT in-place (supported use-case) by updating its header and
+        // internal structures to make use of the whole self.fdt slice but performs no accesses
+        // outside of it and leaves the DT in a state that will be detected by other functions.
+        let ret = unsafe {
+            libfdt_bindgen::fdt_open_into(
+                self.as_ptr(),
+                self.as_mut_ptr(),
+                self.capacity().try_into().map_err(|_| FdtError::Internal)?,
+            )
+        };
+        fdt_err_expect_zero(ret)
+    }
+
+    /// Pack the DT to take a minimum amount of memory.
+    ///
+    /// Doesn't shrink the underlying memory slice.
+    pub fn pack(&mut self) -> Result<()> {
+        // SAFETY - "Closes" the DT in-place by updating its header and relocating its structs.
+        let ret = unsafe { libfdt_bindgen::fdt_pack(self.as_mut_ptr()) };
+        fdt_err_expect_zero(ret)
+    }
+
     /// Return an iterator of memory banks specified the "/memory" node.
     ///
     /// NOTE: This does not support individual "/memory@XXXX" banks.
-    pub fn memory(&self) -> Result<MemRegIterator> {
+    pub fn memory(&self) -> Result<Option<MemRegIterator>> {
         let memory = CStr::from_bytes_with_nul(b"/memory\0").unwrap();
         let device_type = CStr::from_bytes_with_nul(b"memory\0").unwrap();
 
-        let node = self.node(memory)?;
-        if node.device_type()? != device_type {
-            return Err(FdtError::BadValue);
-        }
+        if let Some(node) = self.node(memory)? {
+            if node.device_type()? != Some(device_type) {
+                return Err(FdtError::BadValue);
+            }
+            let reg = node.reg()?.ok_or(FdtError::BadValue)?;
 
-        MemRegIterator::new(node.reg()?)
+            Ok(Some(MemRegIterator::new(reg)))
+        } else {
+            Ok(None)
+        }
     }
 
     /// Retrieve the standard /chosen node.
-    pub fn chosen(&self) -> Result<FdtNode> {
+    pub fn chosen(&self) -> Result<Option<FdtNode>> {
         self.node(CStr::from_bytes_with_nul(b"/chosen\0").unwrap())
     }
 
     /// Get the root node of the tree.
     pub fn root(&self) -> Result<FdtNode> {
-        self.node(CStr::from_bytes_with_nul(b"/\0").unwrap())
+        self.node(CStr::from_bytes_with_nul(b"/\0").unwrap())?.ok_or(FdtError::Internal)
     }
 
     /// Find a tree node by its full path.
-    pub fn node(&self, path: &CStr) -> Result<FdtNode> {
-        let offset = self.path_offset(path)?;
-        Ok(FdtNode { fdt: self, offset })
+    pub fn node(&self, path: &CStr) -> Result<Option<FdtNode>> {
+        Ok(self.path_offset(path)?.map(|offset| FdtNode { fdt: self, offset }))
     }
 
     /// Iterate over nodes with a given compatible string.
@@ -473,7 +616,17 @@
         CompatibleIterator::new(self, compatible)
     }
 
-    fn path_offset(&self, path: &CStr) -> Result<c_int> {
+    /// Get the mutable root node of the tree.
+    pub fn root_mut(&mut self) -> Result<FdtNodeMut> {
+        self.node_mut(CStr::from_bytes_with_nul(b"/\0").unwrap())?.ok_or(FdtError::Internal)
+    }
+
+    /// Find a mutable tree node by its full path.
+    pub fn node_mut(&mut self, path: &CStr) -> Result<Option<FdtNodeMut>> {
+        Ok(self.path_offset(path)?.map(|offset| FdtNodeMut { fdt: self, offset }))
+    }
+
+    fn path_offset(&self, path: &CStr) -> Result<Option<c_int>> {
         let len = path.to_bytes().len().try_into().map_err(|_| FdtError::BadPath)?;
         // SAFETY - Accesses are constrained to the DT totalsize (validated by ctor) and the
         // function respects the passed number of characters.
@@ -482,11 +635,11 @@
             libfdt_bindgen::fdt_path_offset_namelen(self.as_ptr(), path.as_ptr(), len)
         };
 
-        fdt_err(ret)
+        fdt_err_or_option(ret)
     }
 
     fn check_full(&self) -> Result<()> {
-        let len = self.bytes.len();
+        let len = self.buffer.len();
         // SAFETY - Only performs read accesses within the limits of the slice. If successful, this
         // call guarantees to other unsafe calls that the header contains a valid totalsize (w.r.t.
         // 'len' i.e. the self.fdt slice) that those C functions can use to perform bounds
@@ -499,4 +652,12 @@
     fn as_ptr(&self) -> *const c_void {
         self as *const _ as *const c_void
     }
+
+    fn as_mut_ptr(&mut self) -> *mut c_void {
+        self as *mut _ as *mut c_void
+    }
+
+    fn capacity(&self) -> usize {
+        self.buffer.len()
+    }
 }
diff --git a/microdroid/init.rc b/microdroid/init.rc
index 94ef940..7d04557 100644
--- a/microdroid/init.rc
+++ b/microdroid/init.rc
@@ -74,6 +74,12 @@
     # some services can be started.
     trigger late-fs
 
+    # Wait for microdroid_manager to finish setting up sysprops from the payload config.
+    # Some further actions in the boot sequence might depend on the sysprops from the payloag,
+    # e.g. microdroid.config.enable_authfs configures whether to run authfs_service after
+    # /data is mounted.
+    wait_for_prop microdroid_manager.config_done 1
+
     trigger post-fs-data
 
     # Load persist properties and override properties (if enabled) from /data.
@@ -132,6 +138,13 @@
     mkdir /data/local 0751 root root
     mkdir /data/local/tmp 0771 shell shell
 
+on post-fs-data && property:microdroid_manager.authfs.enabled=1
+    start authfs_service
+
+on boot
+    # Mark boot completed. This will notify microdroid_manager to run payload.
+    setprop dev.bootcomplete 1
+
 service tombstone_transmit /system/bin/tombstone_transmit.microdroid -cid 2 -port 2000 -remove_tombstones_after_transmitting
     user system
     group system
diff --git a/microdroid_manager/src/main.rs b/microdroid_manager/src/main.rs
index a706dbe..4f94bb4 100644
--- a/microdroid_manager/src/main.rs
+++ b/microdroid_manager/src/main.rs
@@ -85,7 +85,7 @@
 const ENCRYPTEDSTORE_BACKING_DEVICE: &str = "/dev/block/by-name/encryptedstore";
 const ENCRYPTEDSTORE_BIN: &str = "/system/bin/encryptedstore";
 const ENCRYPTEDSTORE_KEY_IDENTIFIER: &str = "encryptedstore_key";
-const ENCRYPTEDSTORE_KEYSIZE: u32 = 64;
+const ENCRYPTEDSTORE_KEYSIZE: u32 = 32;
 const ENCRYPTEDSTORE_MOUNTPOINT: &str = "/mnt/encryptedstore";
 
 #[derive(thiserror::Error, Debug)]
@@ -417,9 +417,10 @@
     mount_extra_apks(&config)?;
 
     // Wait until apex config is done. (e.g. linker configuration for apexes)
-    // TODO(jooyung): wait until sys.boot_completed?
     wait_for_apex_config_done()?;
 
+    setup_config_sysprops(&config)?;
+
     // Start tombstone_transmit if enabled
     if config.export_tombstones {
         control_service("start", "tombstone_transmit")?;
@@ -427,11 +428,6 @@
         control_service("stop", "tombstoned")?;
     }
 
-    // Start authfs if enabled
-    if config.enable_authfs {
-        control_service("start", "authfs_service")?;
-    }
-
     // Wait until zipfuse has mounted the APK so we can access the payload
     wait_for_property_true(APK_MOUNT_DONE_PROP).context("Failed waiting for APK mount done")?;
 
@@ -442,7 +438,8 @@
         ensure!(exitcode.success(), "Unable to prepare encrypted storage. Exitcode={}", exitcode);
     }
 
-    system_properties::write("dev.bootcomplete", "1").context("set dev.bootcomplete")?;
+    wait_for_property_true("dev.bootcomplete").context("failed waiting for dev.bootcomplete")?;
+    info!("boot completed, time to run payload");
     exec_task(task, service).context("Failed to run payload")
 }
 
@@ -682,6 +679,16 @@
     Ok(())
 }
 
+fn setup_config_sysprops(config: &VmPayloadConfig) -> Result<()> {
+    if config.enable_authfs {
+        system_properties::write("microdroid_manager.authfs.enabled", "1")
+            .context("failed to write microdroid_manager.authfs.enabled")?;
+    }
+    system_properties::write("microdroid_manager.config_done", "1")
+        .context("failed to write microdroid_manager.config_done")?;
+    Ok(())
+}
+
 // Waits until linker config is generated
 fn wait_for_apex_config_done() -> Result<()> {
     wait_for_property_true(APEX_CONFIG_DONE_PROP).context("Failed waiting for apex config done")
diff --git a/pvmfw/Android.bp b/pvmfw/Android.bp
index 77de696..b78e077 100644
--- a/pvmfw/Android.bp
+++ b/pvmfw/Android.bp
@@ -12,11 +12,17 @@
         "legacy",
     ],
     rustlibs: [
+        "libaarch64_paging",
         "libbuddy_system_allocator",
+        "liblibfdt",
         "liblog_rust_nostd",
         "libpvmfw_embedded_key",
+        "libtinyvec_nostd",
         "libvmbase",
     ],
+    static_libs: [
+        "libarm-optimized-routines-mem",
+    ],
     apex_available: ["com.android.virt"],
 }
 
diff --git a/pvmfw/idmap.S b/pvmfw/idmap.S
index ec3ceaf..2ef0d42 100644
--- a/pvmfw/idmap.S
+++ b/pvmfw/idmap.S
@@ -40,13 +40,9 @@
 	/* level 1 */
 	.quad		.L_BLOCK_DEV | 0x0		// 1 GB of device mappings
 	.quad		.L_TT_TYPE_TABLE + 0f		// Unmapped device memory, and pVM firmware
-	.quad		.L_TT_TYPE_TABLE + 1f		// up to 1 GB of DRAM
-	.fill		509, 8, 0x0			// 509 GB of remaining VA space
+	.fill		510, 8, 0x0			// 510 GB of remaining VA space
 
 	/* level 2 */
 0:	.fill		510, 8, 0x0
 	.quad		.L_BLOCK_MEM_XIP | 0x7fc00000	// pVM firmware image
 	.quad		.L_BLOCK_MEM	 | 0x7fe00000	// Writable memory for stack, heap &c.
-1:	.quad		.L_BLOCK_RO	 | 0x80000000	// DT provided by VMM
-	.quad		.L_BLOCK_RO	 | 0x80200000	// 2 MB of DRAM containing payload image
-	.fill		510, 8, 0x0
diff --git a/pvmfw/src/config.rs b/pvmfw/src/config.rs
new file mode 100644
index 0000000..0f2a39c
--- /dev/null
+++ b/pvmfw/src/config.rs
@@ -0,0 +1,200 @@
+// Copyright 2022, 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.
+
+//! Support for the pvmfw configuration data format.
+
+use crate::helpers;
+use core::fmt;
+use core::mem;
+use core::num::NonZeroUsize;
+use core::ops;
+use core::result;
+
+#[repr(C, packed)]
+#[derive(Clone, Copy, Debug)]
+struct Header {
+    magic: u32,
+    version: u32,
+    total_size: u32,
+    flags: u32,
+    entries: [HeaderEntry; Entry::COUNT],
+}
+
+#[derive(Debug)]
+pub enum Error {
+    /// Reserved region can't fit configuration header.
+    BufferTooSmall,
+    /// Header doesn't contain the expect magic value.
+    InvalidMagic,
+    /// Version of the header isn't supported.
+    UnsupportedVersion(u16, u16),
+    /// Header sets flags incorrectly or uses reserved flags.
+    InvalidFlags(u32),
+    /// Header describes configuration data that doesn't fit in the expected buffer.
+    InvalidSize(usize),
+    /// Header entry is invalid.
+    InvalidEntry(Entry),
+}
+
+impl fmt::Display for Error {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Self::BufferTooSmall => write!(f, "Reserved region is smaller than config header"),
+            Self::InvalidMagic => write!(f, "Wrong magic number"),
+            Self::UnsupportedVersion(x, y) => write!(f, "Version {x}.{y} not supported"),
+            Self::InvalidFlags(v) => write!(f, "Flags value {v:#x} is incorrect or reserved"),
+            Self::InvalidSize(sz) => write!(f, "Total size ({sz:#x}) overflows reserved region"),
+            Self::InvalidEntry(e) => write!(f, "Entry {e:?} is invalid"),
+        }
+    }
+}
+
+pub type Result<T> = result::Result<T, Error>;
+
+impl Header {
+    const MAGIC: u32 = u32::from_ne_bytes(*b"pvmf");
+    const PADDED_SIZE: usize =
+        helpers::unchecked_align_up(mem::size_of::<Self>(), mem::size_of::<u64>());
+
+    pub const fn version(major: u16, minor: u16) -> u32 {
+        ((major as u32) << 16) | (minor as u32)
+    }
+
+    pub const fn version_tuple(&self) -> (u16, u16) {
+        ((self.version >> 16) as u16, self.version as u16)
+    }
+
+    pub fn total_size(&self) -> usize {
+        self.total_size as usize
+    }
+
+    pub fn body_size(&self) -> usize {
+        self.total_size() - Self::PADDED_SIZE
+    }
+
+    fn get(&self, entry: Entry) -> HeaderEntry {
+        self.entries[entry as usize]
+    }
+}
+
+#[derive(Clone, Copy, Debug)]
+pub enum Entry {
+    Bcc = 0,
+    DebugPolicy = 1,
+}
+
+impl Entry {
+    const COUNT: usize = 2;
+}
+
+#[repr(packed)]
+#[derive(Clone, Copy, Debug)]
+struct HeaderEntry {
+    offset: u32,
+    size: u32,
+}
+
+impl HeaderEntry {
+    pub fn is_empty(&self) -> bool {
+        self.offset() == 0 && self.size() == 0
+    }
+
+    pub fn fits_in(&self, max_size: usize) -> bool {
+        (Header::PADDED_SIZE..max_size).contains(&self.offset())
+            && NonZeroUsize::new(self.size())
+                .and_then(|s| s.checked_add(self.offset()))
+                .filter(|&x| x.get() <= max_size)
+                .is_some()
+    }
+
+    pub fn as_body_range(&self) -> ops::Range<usize> {
+        let start = self.offset() - Header::PADDED_SIZE;
+
+        start..(start + self.size())
+    }
+
+    pub fn offset(&self) -> usize {
+        self.offset as usize
+    }
+
+    pub fn size(&self) -> usize {
+        self.size as usize
+    }
+}
+
+#[derive(Debug)]
+pub struct Config<'a> {
+    header: &'a Header,
+    body: &'a mut [u8],
+}
+
+impl<'a> Config<'a> {
+    /// Take ownership of a pvmfw configuration consisting of its header and following entries.
+    ///
+    /// SAFETY - 'data' should respect the alignment of Header.
+    pub unsafe fn new(data: &'a mut [u8]) -> Result<Self> {
+        let header = data.get(..Header::PADDED_SIZE).ok_or(Error::BufferTooSmall)?;
+
+        let header = &*(header.as_ptr() as *const Header);
+
+        if header.magic != Header::MAGIC {
+            return Err(Error::InvalidMagic);
+        }
+
+        if header.version != Header::version(1, 0) {
+            let (major, minor) = header.version_tuple();
+            return Err(Error::UnsupportedVersion(major, minor));
+        }
+
+        if header.flags != 0 {
+            return Err(Error::InvalidFlags(header.flags));
+        }
+
+        let total_size = header.total_size();
+
+        // BCC is a mandatory entry of the configuration data.
+        if !header.get(Entry::Bcc).fits_in(total_size) {
+            return Err(Error::InvalidEntry(Entry::Bcc));
+        }
+
+        // Debug policy is optional.
+        let dp = header.get(Entry::DebugPolicy);
+        if !dp.is_empty() && !dp.fits_in(total_size) {
+            return Err(Error::InvalidEntry(Entry::DebugPolicy));
+        }
+
+        let body = data
+            .get_mut(Header::PADDED_SIZE..)
+            .ok_or(Error::BufferTooSmall)?
+            .get_mut(..header.body_size())
+            .ok_or(Error::InvalidSize(total_size))?;
+
+        Ok(Self { header, body })
+    }
+
+    /// Get slice containing the platform BCC.
+    pub fn get_bcc_mut(&mut self) -> &mut [u8] {
+        &mut self.body[self.header.get(Entry::Bcc).as_body_range()]
+    }
+
+    /// Get slice containing the platform debug policy.
+    pub fn get_debug_policy(&mut self) -> Option<&mut [u8]> {
+        let entry = self.header.get(Entry::DebugPolicy);
+        if entry.is_empty() {
+            None
+        } else {
+            Some(&mut self.body[entry.as_body_range()])
+        }
+    }
+}
diff --git a/pvmfw/src/entry.rs b/pvmfw/src/entry.rs
index c0ad878..b840488 100644
--- a/pvmfw/src/entry.rs
+++ b/pvmfw/src/entry.rs
@@ -14,13 +14,20 @@
 
 //! Low-level entry and exit points of pvmfw.
 
+use crate::config;
+use crate::fdt;
 use crate::heap;
 use crate::helpers;
+use crate::memory::MemoryTracker;
 use crate::mmio_guard;
+use crate::mmu;
 use core::arch::asm;
+use core::num::NonZeroUsize;
 use core::slice;
 use log::debug;
 use log::error;
+use log::info;
+use log::warn;
 use log::LevelFilter;
 use vmbase::{console, layout, logger, main, power::reboot};
 
@@ -28,8 +35,16 @@
 enum RebootReason {
     /// A malformed BCC was received.
     InvalidBcc,
+    /// An invalid configuration was appended to pvmfw.
+    InvalidConfig,
     /// An unexpected internal error happened.
     InternalError,
+    /// The provided FDT was invalid.
+    InvalidFdt,
+    /// The provided payload was invalid.
+    InvalidPayload,
+    /// The provided ramdisk was invalid.
+    InvalidRamdisk,
 }
 
 main!(start);
@@ -48,6 +63,98 @@
     // if we reach this point and return, vmbase::entry::rust_entry() will call power::shutdown().
 }
 
+struct MemorySlices<'a> {
+    fdt: &'a mut libfdt::Fdt,
+    kernel: &'a [u8],
+    ramdisk: Option<&'a [u8]>,
+}
+
+impl<'a> MemorySlices<'a> {
+    fn new(
+        fdt: usize,
+        payload: usize,
+        payload_size: usize,
+        memory: &mut MemoryTracker,
+    ) -> Result<Self, RebootReason> {
+        // SAFETY - SIZE_2MB is non-zero.
+        const FDT_SIZE: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(helpers::SIZE_2MB) };
+        // 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.alloc_mut(fdt, FDT_SIZE).map_err(|e| {
+            error!("Failed to allocate the FDT range: {e}");
+            RebootReason::InternalError
+        })?;
+
+        // SAFETY - The tracker validated the range to be in main memory, mapped, and not overlap.
+        let fdt = unsafe { slice::from_raw_parts_mut(range.start as *mut u8, range.len()) };
+        let fdt = libfdt::Fdt::from_mut_slice(fdt).map_err(|e| {
+            error!("Failed to spawn the FDT wrapper: {e}");
+            RebootReason::InvalidFdt
+        })?;
+
+        debug!("Fdt passed validation!");
+
+        let memory_range = fdt
+            .memory()
+            .map_err(|e| {
+                error!("Failed to get /memory from the DT: {e}");
+                RebootReason::InvalidFdt
+            })?
+            .ok_or_else(|| {
+                error!("Node /memory was found empty");
+                RebootReason::InvalidFdt
+            })?
+            .next()
+            .ok_or_else(|| {
+                error!("Failed to read the memory size from the FDT");
+                RebootReason::InternalError
+            })?;
+
+        debug!("Resizing MemoryTracker to range {memory_range:#x?}");
+
+        memory.shrink(&memory_range).map_err(|_| {
+            error!("Failed to use memory range value from DT: {memory_range:#x?}");
+            RebootReason::InvalidFdt
+        })?;
+
+        let payload_size = NonZeroUsize::new(payload_size).ok_or_else(|| {
+            error!("Invalid payload size: {payload_size:#x}");
+            RebootReason::InvalidPayload
+        })?;
+
+        let payload_range = memory.alloc(payload, payload_size).map_err(|e| {
+            error!("Failed to obtain the payload range: {e}");
+            RebootReason::InternalError
+        })?;
+        // SAFETY - The tracker validated the range to be in main memory, mapped, and not overlap.
+        let kernel =
+            unsafe { slice::from_raw_parts(payload_range.start as *const u8, payload_range.len()) };
+
+        let ramdisk_range = fdt::initrd_range(fdt).map_err(|e| {
+            error!("An error occurred while locating the ramdisk in the device tree: {e}");
+            RebootReason::InternalError
+        })?;
+
+        let ramdisk = if let Some(r) = ramdisk_range {
+            debug!("Located ramdisk at {r:?}");
+            let r = memory.alloc_range(&r).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.
+            Some(unsafe { slice::from_raw_parts(r.start as *const u8, r.len()) })
+        } else {
+            info!("Couldn't locate the ramdisk from the device tree");
+            None
+        };
+
+        Ok(Self { fdt, kernel, ramdisk })
+    }
+}
+
 /// Sets up the environment for main() and wraps its result for start().
 ///
 /// Provide the abstractions necessary for start() to abort the pVM boot and for main() to run with
@@ -63,14 +170,6 @@
 
     logger::init(LevelFilter::Info).map_err(|_| RebootReason::InternalError)?;
 
-    const FDT_MAX_SIZE: usize = helpers::SIZE_2MB;
-    // TODO: Check that the FDT is fully contained in RAM.
-    // SAFETY - We trust the VMM, for now.
-    let fdt = unsafe { slice::from_raw_parts_mut(fdt as *mut u8, FDT_MAX_SIZE) };
-    // TODO: Check that the payload is fully contained in RAM and doesn't overlap with the FDT.
-    // SAFETY - We trust the VMM, for now.
-    let payload = unsafe { slice::from_raw_parts(payload as *const u8, payload_size) };
-
     // Use debug!() to avoid printing to the UART if we failed to configure it as only local
     // builds that have tweaked the logger::init() call will actually attempt to log the message.
 
@@ -86,13 +185,45 @@
 
     // SAFETY - We only get the appended payload from here, once. It is mapped and the linker
     // script prevents it from overlapping with other objects.
-    let bcc = as_bcc(unsafe { get_appended_data_slice() }).ok_or_else(|| {
+    let appended_data = unsafe { get_appended_data_slice() };
+
+    // Up to this point, we were using the built-in static (from .rodata) page tables.
+
+    let mut page_table = mmu::PageTable::from_static_layout().map_err(|e| {
+        error!("Failed to set up the dynamic page tables: {e}");
+        RebootReason::InternalError
+    })?;
+
+    const CONSOLE_LEN: usize = 1; // vmbase::uart::Uart only uses one u8 register.
+    let uart_range = console::BASE_ADDRESS..(console::BASE_ADDRESS + CONSOLE_LEN);
+    page_table.map_device(&uart_range).map_err(|e| {
+        error!("Failed to remap the UART as a dynamic page table entry: {e}");
+        RebootReason::InternalError
+    })?;
+
+    // SAFETY - We only get the appended payload from here, once. It is statically mapped and the
+    // linker script prevents it from overlapping with other objects.
+    let mut appended = unsafe { AppendedPayload::new(appended_data) }.ok_or_else(|| {
+        error!("No valid configuration found");
+        RebootReason::InvalidConfig
+    })?;
+
+    let bcc = appended.get_bcc_mut().ok_or_else(|| {
         error!("Invalid BCC");
         RebootReason::InvalidBcc
     })?;
 
+    debug!("Activating dynamic page table...");
+    // SAFETY - page_table duplicates the static mappings for everything that the Rust code is
+    // aware of so activating it shouldn't have any visible effect.
+    unsafe { page_table.activate() };
+    debug!("... Success!");
+
+    let mut memory = MemoryTracker::new(page_table);
+    let slices = MemorySlices::new(fdt, payload, payload_size, &mut memory)?;
+
     // This wrapper allows main() to be blissfully ignorant of platform details.
-    crate::main(fdt, payload, bcc);
+    crate::main(slices.fdt, slices.kernel, slices.ramdisk, bcc);
 
     // TODO: Overwrite BCC before jumping to payload to avoid leaking our sealing key.
 
@@ -169,13 +300,50 @@
     slice::from_raw_parts_mut(base as *mut u8, size)
 }
 
-fn as_bcc(data: &mut [u8]) -> Option<&mut [u8]> {
-    const BCC_SIZE: usize = helpers::SIZE_4KB;
+enum AppendedPayload<'a> {
+    /// Configuration data.
+    Config(config::Config<'a>),
+    /// Deprecated raw BCC, as used in Android T.
+    LegacyBcc(&'a mut [u8]),
+}
 
-    if cfg!(feature = "legacy") {
+impl<'a> AppendedPayload<'a> {
+    /// SAFETY - 'data' should respect the alignment of config::Header.
+    unsafe fn new(data: &'a mut [u8]) -> Option<Self> {
+        if Self::is_valid_config(data) {
+            Some(Self::Config(config::Config::new(data).unwrap()))
+        } else if cfg!(feature = "legacy") {
+            const BCC_SIZE: usize = helpers::SIZE_4KB;
+            warn!("Assuming the appended data at {:?} to be a raw BCC", data.as_ptr());
+            Some(Self::LegacyBcc(&mut data[..BCC_SIZE]))
+        } else {
+            None
+        }
+    }
+
+    unsafe fn is_valid_config(data: &mut [u8]) -> bool {
+        // This function is necessary to prevent the borrow checker from getting confused
+        // about the ownership of data in new(); see https://users.rust-lang.org/t/78467.
+        let addr = data.as_ptr();
+        config::Config::new(data)
+            .map_err(|e| warn!("Invalid configuration data at {addr:?}: {e}"))
+            .is_ok()
+    }
+
+    #[allow(dead_code)] // TODO(b/232900974)
+    fn get_debug_policy(&mut self) -> Option<&mut [u8]> {
+        match self {
+            Self::Config(ref mut cfg) => cfg.get_debug_policy(),
+            Self::LegacyBcc(_) => None,
+        }
+    }
+
+    fn get_bcc_mut(&mut self) -> Option<&mut [u8]> {
+        let bcc = match self {
+            Self::LegacyBcc(ref mut bcc) => bcc,
+            Self::Config(ref mut cfg) => cfg.get_bcc_mut(),
+        };
         // TODO(b/256148034): return None if BccHandoverParse(bcc) != kDiceResultOk.
-        Some(&mut data[..BCC_SIZE])
-    } else {
-        None
+        Some(bcc)
     }
 }
diff --git a/pvmfw/src/fdt.rs b/pvmfw/src/fdt.rs
new file mode 100644
index 0000000..5b9efd2
--- /dev/null
+++ b/pvmfw/src/fdt.rs
@@ -0,0 +1,32 @@
+// Copyright 2022, 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.
+
+//! High-level FDT functions.
+
+use core::ffi::CStr;
+use core::ops::Range;
+
+/// Extract from /chosen the address range containing the pre-loaded ramdisk.
+pub fn initrd_range(fdt: &libfdt::Fdt) -> libfdt::Result<Option<Range<usize>>> {
+    let start = CStr::from_bytes_with_nul(b"linux,initrd-start\0").unwrap();
+    let end = CStr::from_bytes_with_nul(b"linux,initrd-end\0").unwrap();
+
+    if let Some(chosen) = fdt.chosen()? {
+        if let (Some(start), Some(end)) = (chosen.getprop_u32(start)?, chosen.getprop_u32(end)?) {
+            return Ok(Some((start as usize)..(end as usize)));
+        }
+    }
+
+    Ok(None)
+}
diff --git a/pvmfw/src/helpers.rs b/pvmfw/src/helpers.rs
index 59cf9f3..f1ff36d 100644
--- a/pvmfw/src/helpers.rs
+++ b/pvmfw/src/helpers.rs
@@ -14,6 +14,8 @@
 
 //! Miscellaneous helper functions.
 
+use core::arch::asm;
+
 pub const SIZE_4KB: usize = 4 << 10;
 pub const SIZE_2MB: usize = 2 << 20;
 
@@ -24,6 +26,13 @@
     addr & !(alignment - 1)
 }
 
+/// Computes the smallest multiple of the provided alignment larger or equal to the address.
+///
+/// Note: the result is undefined if alignment isn't a power of two and may wrap to 0.
+pub const fn unchecked_align_up(addr: usize, alignment: usize) -> usize {
+    unchecked_align_down(addr + alignment - 1, alignment)
+}
+
 /// Safe wrapper around unchecked_align_up() that validates its assumptions and doesn't wrap.
 pub const fn align_up(addr: usize, alignment: usize) -> Option<usize> {
     if !alignment.is_power_of_two() {
@@ -39,3 +48,30 @@
 pub const fn page_4kb_of(addr: usize) -> usize {
     unchecked_align_down(addr, SIZE_4KB)
 }
+
+#[inline]
+fn min_dcache_line_size() -> usize {
+    const DMINLINE_SHIFT: usize = 16;
+    const DMINLINE_MASK: usize = 0xf;
+    let ctr_el0: usize;
+
+    unsafe { asm!("mrs {x}, ctr_el0", x = out(reg) ctr_el0) }
+
+    // DminLine: log2 of the number of words in the smallest cache line of all the data caches.
+    let dminline = (ctr_el0 >> DMINLINE_SHIFT) & DMINLINE_MASK;
+
+    1 << dminline
+}
+
+/// Flush `size` bytes of data cache by virtual address.
+#[inline]
+pub fn flush_region(start: usize, size: usize) {
+    let line_size = min_dcache_line_size();
+    let end = start + size;
+    let start = unchecked_align_down(start, line_size);
+
+    for line in (start..end).step_by(line_size) {
+        // SAFETY - Clearing cache lines shouldn't have Rust-visible side effects.
+        unsafe { asm!("dc cvau, {x}", x = in(reg) line) }
+    }
+}
diff --git a/pvmfw/src/main.rs b/pvmfw/src/main.rs
index 8178d0b..6810fda 100644
--- a/pvmfw/src/main.rs
+++ b/pvmfw/src/main.rs
@@ -17,26 +17,32 @@
 #![no_main]
 #![no_std]
 #![feature(default_alloc_error_handler)]
+#![feature(ptr_const_cast)] // Stabilized in 1.65.0
 
 mod avb;
+mod config;
 mod entry;
 mod exceptions;
+mod fdt;
 mod heap;
 mod helpers;
+mod memory;
 mod mmio_guard;
+mod mmu;
 mod smccc;
 
 use avb::PUBLIC_KEY;
 use log::{debug, info};
 
-fn main(fdt: &mut [u8], payload: &[u8], bcc: &[u8]) {
+fn main(fdt: &libfdt::Fdt, signed_kernel: &[u8], ramdisk: Option<&[u8]>, bcc: &[u8]) {
     info!("pVM firmware");
-    debug!(
-        "fdt_address={:#018x}, payload_start={:#018x}, payload_size={:#018x}",
-        fdt.as_ptr() as usize,
-        payload.as_ptr() as usize,
-        payload.len(),
-    );
+    debug!("FDT: {:?}", fdt as *const libfdt::Fdt);
+    debug!("Signed kernel: {:?} ({:#x} bytes)", signed_kernel.as_ptr(), signed_kernel.len());
+    if let Some(rd) = ramdisk {
+        debug!("Ramdisk: {:?} ({:#x} bytes)", rd.as_ptr(), rd.len());
+    } else {
+        debug!("Ramdisk: None");
+    }
     debug!("BCC: {:?} ({:#x} bytes)", bcc.as_ptr(), bcc.len());
     debug!("AVB public key: addr={:?}, size={:#x} ({1})", PUBLIC_KEY.as_ptr(), PUBLIC_KEY.len());
     info!("Starting payload...");
diff --git a/pvmfw/src/memory.rs b/pvmfw/src/memory.rs
new file mode 100644
index 0000000..ca0b886
--- /dev/null
+++ b/pvmfw/src/memory.rs
@@ -0,0 +1,190 @@
+// Copyright 2022, 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 allocation and tracking of main memory.
+
+use crate::helpers;
+use crate::mmu;
+use core::cmp::max;
+use core::cmp::min;
+use core::fmt;
+use core::num::NonZeroUsize;
+use core::ops::Range;
+use core::result;
+use log::error;
+use tinyvec::ArrayVec;
+
+type MemoryRange = Range<usize>;
+
+#[derive(Clone, Copy, Debug, Default)]
+enum MemoryType {
+    #[default]
+    ReadOnly,
+    ReadWrite,
+}
+
+#[derive(Clone, Debug, Default)]
+struct MemoryRegion {
+    range: MemoryRange,
+    mem_type: MemoryType,
+}
+
+impl MemoryRegion {
+    /// True if the instance overlaps with the passed range.
+    pub fn overlaps(&self, range: &MemoryRange) -> bool {
+        let our: &MemoryRange = self.as_ref();
+        max(our.start, range.start) < min(our.end, range.end)
+    }
+
+    /// True if the instance is fully contained within the passed range.
+    pub fn is_within(&self, range: &MemoryRange) -> bool {
+        let our: &MemoryRange = self.as_ref();
+        self.as_ref() == &(max(our.start, range.start)..min(our.end, range.end))
+    }
+}
+
+impl AsRef<MemoryRange> for MemoryRegion {
+    fn as_ref(&self) -> &MemoryRange {
+        &self.range
+    }
+}
+
+/// Tracks non-overlapping slices of main memory.
+pub struct MemoryTracker {
+    regions: ArrayVec<[MemoryRegion; MemoryTracker::CAPACITY]>,
+    total: MemoryRange,
+    page_table: mmu::PageTable,
+}
+
+/// Errors for MemoryTracker operations.
+#[derive(Debug, Clone)]
+pub enum MemoryTrackerError {
+    /// Tried to modify the memory base address.
+    DifferentBaseAddress,
+    /// Tried to shrink to a larger memory size.
+    SizeTooLarge,
+    /// Tracked regions would not fit in memory size.
+    SizeTooSmall,
+    /// Reached limit number of tracked regions.
+    Full,
+    /// Region is out of the tracked memory address space.
+    OutOfRange,
+    /// New region overlaps with tracked regions.
+    Overlaps,
+    /// Region couldn't be mapped.
+    FailedToMap,
+}
+
+impl fmt::Display for MemoryTrackerError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            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"),
+            Self::Full => write!(f, "Reached limit number of tracked regions"),
+            Self::OutOfRange => write!(f, "Region is out of the tracked memory address space"),
+            Self::Overlaps => write!(f, "New region overlaps with tracked regions"),
+            Self::FailedToMap => write!(f, "Failed to map the new region"),
+        }
+    }
+}
+
+type Result<T> = result::Result<T, MemoryTrackerError>;
+
+impl MemoryTracker {
+    const CAPACITY: usize = 5;
+    /// Base of the system's contiguous "main" memory.
+    const BASE: usize = 0x8000_0000;
+    /// First address that can't be translated by a level 1 TTBR0_EL1.
+    const MAX_ADDR: usize = 1 << 39;
+
+    /// Create a new instance from an active page table, covering the maximum RAM size.
+    pub fn new(page_table: mmu::PageTable) -> Self {
+        Self { total: Self::BASE..Self::MAX_ADDR, page_table, regions: ArrayVec::new() }
+    }
+
+    /// 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<()> {
+        if range.start != self.total.start {
+            return Err(MemoryTrackerError::DifferentBaseAddress);
+        }
+        if self.total.end < range.end {
+            return Err(MemoryTrackerError::SizeTooLarge);
+        }
+        if !self.regions.iter().all(|r| r.is_within(range)) {
+            return Err(MemoryTrackerError::SizeTooSmall);
+        }
+
+        self.total = range.clone();
+        Ok(())
+    }
+
+    /// Allocate the address range for a const slice; returns None if failed.
+    pub fn alloc_range(&mut self, range: &MemoryRange) -> Result<MemoryRange> {
+        self.page_table.map_rodata(range).map_err(|e| {
+            error!("Error during range allocation: {e}");
+            MemoryTrackerError::FailedToMap
+        })?;
+        self.add(MemoryRegion { range: range.clone(), mem_type: MemoryType::ReadOnly })
+    }
+
+    /// Allocate the address range for a mutable slice; returns None if failed.
+    pub fn alloc_range_mut(&mut self, range: &MemoryRange) -> Result<MemoryRange> {
+        self.page_table.map_data(range).map_err(|e| {
+            error!("Error during mutable range allocation: {e}");
+            MemoryTrackerError::FailedToMap
+        })?;
+        self.add(MemoryRegion { range: range.clone(), mem_type: MemoryType::ReadWrite })
+    }
+
+    /// Allocate the address range for a const slice; returns None if failed.
+    pub 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> {
+        self.alloc_range_mut(&(base..(base + size.get())))
+    }
+
+    fn add(&mut self, region: MemoryRegion) -> Result<MemoryRange> {
+        if !region.is_within(&self.total) {
+            return Err(MemoryTrackerError::OutOfRange);
+        }
+        if self.regions.iter().any(|r| r.overlaps(region.as_ref())) {
+            return Err(MemoryTrackerError::Overlaps);
+        }
+        if self.regions.try_push(region).is_some() {
+            return Err(MemoryTrackerError::Full);
+        }
+
+        Ok(self.regions.last().unwrap().as_ref().clone())
+    }
+}
+
+impl Drop for MemoryTracker {
+    fn drop(&mut self) {
+        for region in self.regions.iter() {
+            match region.mem_type {
+                MemoryType::ReadWrite => {
+                    // TODO: Use page table's dirty bit to only flush pages that were touched.
+                    helpers::flush_region(region.range.start, region.range.len())
+                }
+                MemoryType::ReadOnly => {}
+            }
+        }
+    }
+}
diff --git a/pvmfw/src/mmu.rs b/pvmfw/src/mmu.rs
new file mode 100644
index 0000000..fa94e85
--- /dev/null
+++ b/pvmfw/src/mmu.rs
@@ -0,0 +1,86 @@
+// Copyright 2022, 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.
+
+//! Memory management.
+
+use crate::helpers;
+use aarch64_paging::idmap::IdMap;
+use aarch64_paging::paging::Attributes;
+use aarch64_paging::paging::MemoryRegion;
+use aarch64_paging::MapError;
+use core::ops::Range;
+use vmbase::layout;
+
+// We assume that:
+// - MAIR_EL1.Attr0 = "Device-nGnRE memory" (0b0000_0100)
+// - MAIR_EL1.Attr1 = "Normal memory, Outer & Inner WB Non-transient, R/W-Allocate" (0b1111_1111)
+const MEMORY: Attributes = Attributes::NORMAL.union(Attributes::NON_GLOBAL);
+const DEVICE: Attributes = Attributes::DEVICE_NGNRE.union(Attributes::EXECUTE_NEVER);
+const CODE: Attributes = MEMORY.union(Attributes::READ_ONLY);
+const DATA: Attributes = MEMORY.union(Attributes::EXECUTE_NEVER);
+const RODATA: Attributes = DATA.union(Attributes::READ_ONLY);
+
+/// High-level API for managing MMU mappings.
+pub struct PageTable {
+    idmap: IdMap,
+}
+
+fn appended_payload_range() -> Range<usize> {
+    let start = helpers::align_up(layout::binary_end(), helpers::SIZE_4KB).unwrap();
+    // pvmfw is contained in a 2MiB region so the payload can't be larger than the 2MiB alignment.
+    let end = helpers::align_up(start, helpers::SIZE_2MB).unwrap();
+
+    start..end
+}
+
+impl PageTable {
+    const ASID: usize = 1;
+    const ROOT_LEVEL: usize = 1;
+
+    /// Creates an instance pre-populated with pvmfw's binary layout.
+    pub fn from_static_layout() -> Result<Self, MapError> {
+        let mut page_table = Self { idmap: IdMap::new(Self::ASID, Self::ROOT_LEVEL) };
+
+        page_table.map_code(&layout::text_range())?;
+        page_table.map_data(&layout::writable_region())?;
+        page_table.map_rodata(&layout::rodata_range())?;
+        page_table.map_data(&appended_payload_range())?;
+
+        Ok(page_table)
+    }
+
+    pub unsafe fn activate(&mut self) {
+        self.idmap.activate()
+    }
+
+    pub fn map_device(&mut self, range: &Range<usize>) -> Result<(), MapError> {
+        self.map_range(range, DEVICE)
+    }
+
+    pub fn map_data(&mut self, range: &Range<usize>) -> Result<(), MapError> {
+        self.map_range(range, DATA)
+    }
+
+    pub fn map_code(&mut self, range: &Range<usize>) -> Result<(), MapError> {
+        self.map_range(range, CODE)
+    }
+
+    pub fn map_rodata(&mut self, range: &Range<usize>) -> Result<(), MapError> {
+        self.map_range(range, RODATA)
+    }
+
+    fn map_range(&mut self, range: &Range<usize>, attr: Attributes) -> Result<(), MapError> {
+        self.idmap.map_range(&MemoryRegion::new(range.start, range.end), attr)
+    }
+}
diff --git a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
index 899e47e..8ea5426 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -492,6 +492,7 @@
     })
     public void sameInstanceKeepsSameCdis() throws Exception {
         assumeSupportedKernel();
+        assume().withMessage("Skip on CF. Too Slow. b/257270529").that(isCuttlefish()).isFalse();
 
         grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
         VirtualMachineConfig normalConfig =
diff --git a/vm/Android.bp b/vm/Android.bp
index 7b016d4..95ef082 100644
--- a/vm/Android.bp
+++ b/vm/Android.bp
@@ -17,6 +17,7 @@
         "liblibc",
         "liblog_rust",
         "libmicrodroid_payload_config",
+        "librand",
         "librustutils",
         "libserde_json",
         "libserde",
diff --git a/vm/src/main.rs b/vm/src/main.rs
index 89d56d4..b5046fb 100644
--- a/vm/src/main.rs
+++ b/vm/src/main.rs
@@ -27,7 +27,7 @@
 use clap::Parser;
 use create_idsig::command_create_idsig;
 use create_partition::command_create_partition;
-use run::{command_run, command_run_app};
+use run::{command_run, command_run_app, command_run_microdroid};
 use rustutils::system_properties;
 use std::path::{Path, PathBuf};
 
@@ -110,6 +110,65 @@
         #[clap(long = "extra-idsig")]
         extra_idsigs: Vec<PathBuf>,
     },
+    /// Run a virtual machine with Microdroid inside
+    RunMicrodroid {
+        /// Path to the directory where VM-related files (e.g. instance.img, apk.idsig, etc.) will
+        /// be stored. If not specified a random directory under /data/local/tmp/microdroid will be
+        /// created and used.
+        #[clap(long)]
+        work_dir: Option<PathBuf>,
+
+        /// Name of VM
+        #[clap(long)]
+        name: Option<String>,
+
+        /// Detach VM from the terminal and run in the background
+        #[clap(short, long)]
+        daemonize: bool,
+
+        /// Path to the file backing the storage.
+        /// Created if the option is used but the path does not exist in the device.
+        #[clap(long)]
+        storage: Option<PathBuf>,
+
+        /// Size of the storage. Used only if --storage is supplied but path does not exist
+        /// Default size is 10*1024*1024
+        #[clap(long)]
+        storage_size: Option<u64>,
+
+        /// Path to file for VM console output.
+        #[clap(long)]
+        console: Option<PathBuf>,
+
+        /// Path to file for VM log output.
+        #[clap(long)]
+        log: Option<PathBuf>,
+
+        /// Path to file where ramdump is recorded on kernel panic
+        #[clap(long)]
+        ramdump: Option<PathBuf>,
+
+        /// Debug level of the VM. Supported values: "none" (default), "app_only", and "full".
+        #[clap(long, default_value = "none", value_parser = parse_debug_level)]
+        debug: DebugLevel,
+
+        /// Run VM in protected mode.
+        #[clap(short, long)]
+        protected: bool,
+
+        /// Memory size (in MiB) of the VM. If unspecified, defaults to the value of `memory_mib`
+        /// in the VM config file.
+        #[clap(short, long)]
+        mem: Option<u32>,
+
+        /// Number of vCPUs in the VM. If unspecified, defaults to 1.
+        #[clap(long)]
+        cpus: Option<u32>,
+
+        /// Comma separated list of task profile names to apply to the VM
+        #[clap(long)]
+        task_profiles: Vec<String>,
+    },
     /// Run a virtual machine
     Run {
         /// Path to VM config JSON
@@ -238,6 +297,36 @@
             task_profiles,
             &extra_idsigs,
         ),
+        Opt::RunMicrodroid {
+            name,
+            work_dir,
+            storage,
+            storage_size,
+            daemonize,
+            console,
+            log,
+            ramdump,
+            debug,
+            protected,
+            mem,
+            cpus,
+            task_profiles,
+        } => command_run_microdroid(
+            name,
+            service.as_ref(),
+            work_dir,
+            storage.as_deref(),
+            storage_size,
+            daemonize,
+            console.as_deref(),
+            log.as_deref(),
+            ramdump.as_deref(),
+            debug,
+            protected,
+            mem,
+            cpus,
+            task_profiles,
+        ),
         Opt::Run { name, config, daemonize, cpus, task_profiles, console, log } => {
             command_run(
                 name,
diff --git a/vm/src/run.rs b/vm/src/run.rs
index 01b916b..3f25bba 100644
--- a/vm/src/run.rs
+++ b/vm/src/run.rs
@@ -23,13 +23,16 @@
     VirtualMachinePayloadConfig::VirtualMachinePayloadConfig,
     VirtualMachineState::VirtualMachineState,
 };
-use anyhow::{bail, Context, Error};
+use anyhow::{anyhow, bail, Context, Error};
 use binder::ParcelFileDescriptor;
 use microdroid_payload_config::VmPayloadConfig;
+use rand::{distributions::Alphanumeric, Rng};
+use std::fs;
 use std::fs::File;
 use std::io;
 use std::os::unix::io::{AsRawFd, FromRawFd};
 use std::path::{Path, PathBuf};
+use std::process::Command;
 use vmclient::{ErrorCode, VmInstance};
 use vmconfig::{open_parcel_file, VmConfig};
 use zip::ZipArchive;
@@ -144,6 +147,83 @@
     run(service, &config, &payload_config_str, daemonize, console_path, log_path, ramdump_path)
 }
 
+const EMPTY_PAYLOAD_APK: &str = "com.android.microdroid.empty_payload";
+
+fn find_empty_payload_apk_path() -> Result<PathBuf, Error> {
+    let output = Command::new("/system/bin/pm")
+        .arg("path")
+        .arg(EMPTY_PAYLOAD_APK)
+        .output()
+        .context("failed to execute pm path")?;
+    let output_str = String::from_utf8(output.stdout).context("failed to parse output")?;
+    match output_str.strip_prefix("package:") {
+        None => Err(anyhow!("Unexpected output {}", output_str)),
+        Some(apk_path) => Ok(PathBuf::from(apk_path.trim())),
+    }
+}
+
+fn create_work_dir() -> Result<PathBuf, Error> {
+    let s: String =
+        rand::thread_rng().sample_iter(&Alphanumeric).take(17).map(char::from).collect();
+    let work_dir = PathBuf::from("/data/local/tmp/microdroid").join(s);
+    println!("creating work dir {}", work_dir.display());
+    fs::create_dir_all(&work_dir).context("failed to mkdir")?;
+    Ok(work_dir)
+}
+
+/// Run a VM with Microdroid
+#[allow(clippy::too_many_arguments)]
+pub fn command_run_microdroid(
+    name: Option<String>,
+    service: &dyn IVirtualizationService,
+    work_dir: Option<PathBuf>,
+    storage: Option<&Path>,
+    storage_size: Option<u64>,
+    daemonize: bool,
+    console_path: Option<&Path>,
+    log_path: Option<&Path>,
+    ramdump_path: Option<&Path>,
+    debug_level: DebugLevel,
+    protected: bool,
+    mem: Option<u32>,
+    cpus: Option<u32>,
+    task_profiles: Vec<String>,
+) -> Result<(), Error> {
+    let apk = find_empty_payload_apk_path()
+        .context(anyhow!("failed to find path for {} apk", EMPTY_PAYLOAD_APK))?;
+    println!("found path for {} apk: {}", EMPTY_PAYLOAD_APK, apk.display());
+
+    let work_dir = work_dir.unwrap_or(create_work_dir()?);
+    let idsig = work_dir.join("apk.idsig");
+    println!("apk.idsig path: {}", idsig.display());
+    let instance_img = work_dir.join("instance.img");
+    println!("instance.img path: {}", instance_img.display());
+
+    let payload_path = "MicrodroidEmptyPayloadJniLib.so";
+    let extra_sig = [];
+    command_run_app(
+        name,
+        service,
+        &apk,
+        &idsig,
+        &instance_img,
+        storage,
+        storage_size,
+        /* config_path= */ None,
+        Some(payload_path.to_owned()),
+        daemonize,
+        console_path,
+        log_path,
+        ramdump_path,
+        debug_level,
+        protected,
+        mem,
+        cpus,
+        task_profiles,
+        &extra_sig,
+    )
+}
+
 /// Run a VM from the given configuration file.
 #[allow(clippy::too_many_arguments)]
 pub fn command_run(
diff --git a/vmbase/example/idmap.S b/vmbase/example/idmap.S
index 7fc5d5e..71a6ade 100644
--- a/vmbase/example/idmap.S
+++ b/vmbase/example/idmap.S
@@ -44,7 +44,7 @@
 	.fill		509, 8, 0x0			// 509 GiB of remaining VA space
 
 	/* level 2 */
-0:	.quad		.L_BLOCK_RO  | 0x80000000	// DT provided by VMM
+0:	.quad		.L_BLOCK_MEM | 0x80000000	// DT provided by VMM
 	.quad		.L_BLOCK_MEM_XIP | 0x80200000	// 2 MiB of DRAM containing image
 	.quad		.L_BLOCK_MEM | 0x80400000	// 2 MiB of writable DRAM
 	.fill		509, 8, 0x0
diff --git a/vmbase/example/src/main.rs b/vmbase/example/src/main.rs
index dcff6e1..bb64651 100644
--- a/vmbase/example/src/main.rs
+++ b/vmbase/example/src/main.rs
@@ -151,17 +151,41 @@
 
     let reader = Fdt::from_slice(fdt).unwrap();
     info!("FDT passed verification.");
-    for reg in reader.memory().unwrap() {
+    for reg in reader.memory().unwrap().unwrap() {
         info!("memory @ {reg:#x?}");
     }
 
     let compatible = CStr::from_bytes_with_nul(b"ns16550a\0").unwrap();
 
     for c in reader.compatible_nodes(compatible).unwrap() {
-        let reg = c.reg().unwrap().next().unwrap();
+        let reg = c.reg().unwrap().unwrap().next().unwrap();
         info!("node compatible with '{}' at {reg:?}", compatible.to_str().unwrap());
     }
 
+    let writer = Fdt::from_mut_slice(fdt).unwrap();
+    writer.unpack().unwrap();
+    info!("FDT successfully unpacked.");
+
+    let path = CStr::from_bytes_with_nul(b"/memory\0").unwrap();
+    let mut node = writer.node_mut(path).unwrap().unwrap();
+    let name = CStr::from_bytes_with_nul(b"child\0").unwrap();
+    let mut child = node.add_subnode(name).unwrap();
+    info!("Created subnode '{}/{}'.", path.to_str().unwrap(), name.to_str().unwrap());
+
+    let name = CStr::from_bytes_with_nul(b"str-property\0").unwrap();
+    child.appendprop(name, b"property-value\0").unwrap();
+    info!("Appended property '{}'.", name.to_str().unwrap());
+
+    let name = CStr::from_bytes_with_nul(b"pair-property\0").unwrap();
+    let addr = 0x0123_4567u64;
+    let size = 0x89ab_cdefu64;
+    child.appendprop_addrrange(name, addr, size).unwrap();
+    info!("Appended property '{}'.", name.to_str().unwrap());
+
+    let writer = child.fdt();
+    writer.pack().unwrap();
+    info!("FDT successfully packed.");
+
     info!("FDT checks done.");
 }