Merge "Dice Policy: Add dice chain matching functionality" into main
diff --git a/README.md b/README.md
index eaa2579..3935f93 100644
--- a/README.md
+++ b/README.md
@@ -14,12 +14,14 @@
 For in-depth explanations about individual topics and components, visit the following links.
 
 AVF components:
-
 * [pVM firmware](pvmfw/README.md)
 * [Microdroid](microdroid/README.md)
 * [Microdroid kernel](microdroid/kernel/README.md)
 * [Microdroid payload](microdroid/payload/README.md)
 * [vmbase](vmbase/README.md)
+
+AVF APIs:
+* [Java API](javalib/README.md)
 * [VM Payload API](vm_payload/README.md)
 
 How-Tos:
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 1410534..77ccc1d 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -105,6 +105,9 @@
       "path": "packages/modules/Virtualization/rialto"
     },
     {
+      "path": "packages/modules/Virtualization/service_vm/comm"
+    },
+    {
       "path": "packages/modules/Virtualization/service_vm/requests"
     },
     {
diff --git a/apex/Android.bp b/apex/Android.bp
index a05f7b0..a4c8861 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -58,6 +58,13 @@
         "libvirtualizationservice_jni",
         "libvirtualmachine_jni",
     ],
+    // TODO(b/295593640) Unfortunately these are added to the apex even though they are unused.
+    // Once the build system is fixed, remove this.
+    unwanted_transitive_deps: [
+        "libdrm",
+        "libsso",
+        "libutils",
+    ],
 }
 
 apex_defaults {
diff --git a/javalib/README.md b/javalib/README.md
new file mode 100644
index 0000000..cf7a6cb
--- /dev/null
+++ b/javalib/README.md
@@ -0,0 +1,371 @@
+# Android Virtualization Framework API
+
+These Java APIs allow an app to configure and run a Virtual Machine running
+[Microdroid](../microdroid/README.md) and execute native code from the app (the
+payload) within it.
+
+There is more information on AVF [here](../README.md). To see how to package the
+payload code that is to run inside a VM, and the native API available to it, see
+the [VM Payload API](../vm_payload/README.md)
+
+The API classes are all in the
+[`android.system.virtualmachine`](src/android/system/virtualmachine) package.
+
+Note that these APIs are all `@SystemApi` and require the restricted
+`android.permission.MANAGE_VIRTUAL_MACHINE` permission, so they are not
+available to third party apps.
+
+All of these APIs were introduced in API level 34 (Android 14). The classes may
+not exist in devices running an earlier version.
+
+## Detecting AVF Support
+
+The simplest way to detect whether a device has support for AVF is to retrieve
+an instance of the
+[`VirtualMachineManager`](src/android/system/virtualmachine/VirtualMachineManager.java)
+class; if the result is not `null` then the device has support. You can then
+find out whether protected, non-protected VMs, or both are supported using the
+`getCapabilities()` method. Note that this code requires API level 34 or higher:
+
+```Java
+VirtualMachineManager vmm = context.getSystemService(VirtualMachineManager.class);
+if (vmm == null) {
+    // AVF is not supported.
+} else {
+    // AVF is supported.
+    int capabilities = vmm.getCapabilities();
+    if ((capabilties & CAPABILITY_PROTECTED_VM) != 0) {
+        // Protected VMs supported.
+    }
+    if ((capabilties & CAPABILITY_NON_PROTECTED_VM) != 0) {
+        // Non-Protected VMs supported.
+    }
+}
+```
+
+An alternative for detecting AVF support is to query support for the
+`android.software.virtualization_framework` system feature. This method will
+work on any API level, and return false if it is below 34:
+
+```Java
+if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_VIRTUALIZATION_FRAMEWORK)) {
+    // AVF is supported.
+}
+```
+
+You can also express a dependency on this system feature in your app's manifest
+with a
+[`<uses-feature>`](https://developer.android.com/guide/topics/manifest/uses-feature-element)
+element.
+
+
+## Starting a VM
+
+Once you have an instance of the
+[`VirtualMachineManager`](src/android/system/virtualmachine/VirtualMachineManager.java),
+a VM can be started by:
+- Specifying the desired VM configuration, using a
+  [`VirtualMachineConfig`](src/android/system/virtualmachine/VirtualMachineConfig.java)
+  builder;
+- Creating a new
+  [`VirtualMachine`](src/android/system/virtualmachine/VirtualMachine.java)
+  instance (or retrieving an existing one);
+- Registering to retrieve events from the VM by providing a
+  [`VirtualMachineCallback`](src/android/system/virtualmachine/VirtualMachineCallback.java)
+  (optional, but recommended);
+- Running the VM.
+
+A minimal example might look like this:
+
+```Java
+VirtualMachineConfig config =
+        new VirtualMachineConfig.Builder(this)
+            .setProtectedVm(true)
+            .setPayloadBinaryName("my_payload.so")
+            .build();
+
+VirtualMachine vm = vmm.getOrCreate("my vm", config);
+
+vm.setCallback(executor, new VirtualMachineCallback() {...});
+
+vm.run();
+```
+
+Here we are running a protected VM, which will execute the code in the
+`my_payload.so` file included in your APK.
+
+Information about the VM, including its configuration, is stored in files in
+your app's private data directory. The file names are based on the VM name you
+supply. So once an instance of a VM has been created it can be retrieved by name
+even if the app is restarted or the device is rebooted. Directly inspecting or
+modifying these files is not recommended.
+
+The `getOrCreate()` call will retrieve an existing VM instance if it exists (in
+which case the `config` parameter is ignored), or create a new one
+otherwise. There are also separate `get()` and `create()` methods.
+
+The `run()` method is asynchronous; it returns successfully once the VM is
+starting. You can find out when the VM is ready, or if it fails, via your
+`VirtualMachineCallback` implementation.
+
+## VM Configuration
+
+There are other things that you can specify as part of the
+[`VirtualMachineConfig`](src/android/system/virtualmachine/VirtualMachineConfig.java):
+- Whether the VM should be debuggable. A debuggable VM is not secure, but it
+  does allow access to logs from inside the VM, which can be useful for
+  troubleshooting.
+- How much memory should be available to the VM. (This is an upper bound;
+  typically memory is allocated to the VM as it is needed until the limit is
+  reached - but there is some overhead proportional to the maximum size.)
+- How many virtual CPUs the VM has.
+- How much encrypted storage the VM has.
+- The path to the installed APK containing the code to run as the VM
+  payload. (Normally you don't need this; the APK path is determined from the
+  context passed to the config builder.)
+
+## VM Life-cycle
+
+To find out the progress of the Virtual Machine once it is started you should
+implement the methods defined by
+[`VirtualMachineCallback`](src/android/system/virtualmachine/VirtualMachineCallback.java). These
+are called when the following events happen:
+- `onPayloadStarted()`: The VM payload is about to be run.
+- `onPayloadReady()`: The VM payload is running and ready to accept
+  connections. (This notification is triggered by the payload code, using the
+  [`AVmPayload_notifyPayloadReady()`](../vm_payload/include/vm_payload.h)
+  function.)
+- `onPayloadFinished()`: The VM payload has exited normally. The exit code of
+  the VM (the value returned by [`AVmPayload_main()`](../vm_payload/README.md))
+  is supplied as a parameter.
+- `onError()`: The VM failed; something went wrong. An error code and
+  human-readable message are provided which may help diagnosing the problem.
+- `onStopped()`: The VM is no longer running. This is the final notification
+  from any VM run, whether or not it was successful. You can run the VM again
+  when you want to. A reason code indicating why the VM stopped is supplied as a
+  parameter.
+
+You can also query the status of a VM at any point by calling `getStatus()` on
+the `VirtualMachine` object. This will return one of the following values:
+- `STATUS_STOPPED`: The VM is not running - either it has not yet been started,
+  or it stopped after running.
+- `STATUS_RUNNING`: The VM is running. Your payload inside the VM may not be
+  running, since the VM may be in the process of starting or stopping.
+- `STATUS_DELETED`: The VM has been deleted, e.g. by calling the `delete()`
+  method on
+  [`VirtualMachineManager`](src/android/system/virtualmachine/VirtualMachineManager.java). This
+  is irreversible - once a VM is in this state it will never leave it.
+
+Some methods on
+[`VirtualMachine`](src/android/system/virtualmachine/VirtualMachine.java) can
+only be called when the VM status is `STATUS_RUNNING` (e.g. `stop()`), and some
+can only be called when the it is `STATUS_STOPPED` (e.g. `run()`).
+
+## VM Identity and Secrets
+
+Every VM has a 32-byte secret unique to it, which is not available to the
+host. We refer to this as the VM identity.  The secret, and thus the identity,
+doesn’t normally change if the same VM is stopped and started, even after a
+reboot.
+
+In Android 14 the secret is derived, using the [Open Profile for
+DICE](https://pigweed.googlesource.com/open-dice/+/refs/heads/main/docs/android.md),
+from:
+- A device-specific randomly generated value;
+- The complete system image;
+- A per-instance salt;
+- The code running in the VM, including the bootloader, kernel, Microdroid and
+  payload;
+- Significant VM configuration options, e.g.  whether the VM is debuggable.
+
+Any change to any of these will mean a different secret is generated.  So while
+an attacker could start a similar VM with maliciously altered code, that VM will
+not have access to the same secret. An attempt to start an existing VM instance
+which doesn't derive the same secret will fail.
+
+However, this also means that if the payload code changes - for example, your
+app is updated - then this also changes the identity. An existing VM instance
+will no longer be runnable, and you will have to delete it and create a new
+instance with a new secret.
+
+The payload code is not given direct access to the VM secret, but an API is
+provided to allow deterministically deriving further secrets from it,
+e.g. encryption or signing keys. See
+[`AVmPayload_getVmInstanceSecret()`](../vm_payload/include/vm_payload.h).
+
+Some VM configuration changes are allowed that don’t affect the identity -
+e.g. changing the number of CPUs or the amount of memory allocated to the
+VM. This can be done using the `setConfig()` method on
+[`VirtualMachine`](src/android/system/virtualmachine/VirtualMachine.java).
+
+Deleting a VM (using the `delete()` method on
+[`VirtualMachineManager`](src/android/system/virtualmachine/VirtualMachineManager.java))
+and recreating it will generate a new salt, so the new VM will have a different
+secret, even if it is otherwise identical.
+
+## Communicating with a VM
+
+Once the VM payload has successfully started you will probably want to establish
+communication between it and your app.
+
+Only the app that started a VM can connect to it. The VM can accept connections
+from the app, but cannot initiate connections to other VMs or other processes in
+the host Android.
+
+### Vsock
+
+The simplest form of communication is using a socket running over the
+[vsock](https://man7.org/linux/man-pages/man7/vsock.7.html) protocol.
+
+We suggest that the VM payload should create a listening socket (using the
+standard socket API) and then trigger the `onPayloadReady()` callback; the app
+can then connect to the socket. This helps to avoid a race condition where the
+app tries to connect before the VM is listening, necessitating a retry
+mechanism.
+
+In the payload this might look like this:
+
+```C++
+#include "vm_payload.h"
+
+extern "C" int AVmPayload_main() {
+  int fd = socket(AF_VSOCK, SOCK_STREAM, 0);
+  // bind, listen
+  AVmPayload_notifyPayloadReady();
+  // accept, read/write, ...
+}
+```
+
+And, in the app, like this:
+
+```Java
+void onPayloadReady(VirtualMachine vm) {
+  ParcelFileDescriptor pfd = vm.connectVsock(port);
+  // ...
+}
+```
+
+Vsock is useful for simple communication, or transferring of bulk data. For a
+richer RPC style of communication we suggest using Binder.
+
+### Binder
+
+The use of AIDL interfaces between the VM and app is supported via Binder RPC,
+which transmits messages over an underlying vsock socket.
+
+Note that Binder RPC has some limitations compared to the kernel Binder used in
+Android - for example file descriptors can't be sent. It also isn't possible to
+send a kernel Binder interface over Binder RPC, or vice versa.
+
+There is a payload API to allow an AIDL interface to be served over a specific
+vsock port, and the VirtualMachine class provides a way to connect to the VM and
+retrieve an instance of the interface.
+
+The payload code to serve a hypothetical `IPayload` interface might look like
+this:
+
+```C++
+class PayloadImpl : public BnPayload { ... };
+
+
+extern "C" int AVmPayload_main() {
+  auto service = ndk::SharedRefBase::make<PayloadImpl>();
+  auto callback = [](void*) {
+    AVmPayload_notifyPayloadReady();
+  };
+  AVmPayload_runVsockRpcServer(service->asBinder().get(),
+    port, callback, nullptr);
+}
+
+```
+
+And then the app code to connect to it could look like this:
+
+```Java
+void onPayloadReady(VirtualMachine vm) {
+  IPayload payload =
+    Payload.Stub.asInterface(vm.connectToVsockServer(port));
+  // ...
+}
+```
+
+## Stopping a VM
+
+You can stop a VM abruptly by calling the `stop()` method on the
+[`VirtualMachine`](src/android/system/virtualmachine/VirtualMachine.java)
+instance. This is equivalent to turning off the power; the VM gets no
+opportunity to clean up at all. Any unwritten data might be lost.
+
+A better strategy might be to wait for the VM to exit cleanly (e.g. waiting for
+the `onStopped()` callback).
+
+Then you can arrange for your VM payload code to exit when it has finished its
+task (by returning from [`AVmPayload_main()`](../vm_payload/README.md), or
+calling `exit()`). Alternatively you could exit when you receive a request to do
+so from the app, e.g. via binder.
+
+When the VM payload does this you will receive an `onPayloadFinished()`
+callback, if you have installed a
+[`VirtualMachineCallback`](src/android/system/virtualmachine/VirtualMachineCallback.java),
+which includes the payload's exit code.
+
+Use of `stop()` should be reserved as a recovery mechanism - for example if the
+VM has not stopped within a reasonable time (a few seconds, say) after being
+requested to.
+
+The status of a VM will be `STATUS_STOPPED` if your `onStopped()` callback is
+invoked, or after a successful call to `stop()`. Note that your `onStopped()`
+will be called on the VM even if it ended as a result of a call to `stop()`.
+
+# Encrypted Storage
+
+When configuring a VM you can specify that it should have access to an encrypted
+storage filesystem of up to a specified size, using the
+`setEncryptedStorageBytes()` method on a
+[`VirtualMachineConfig`](src/android/system/virtualmachine/VirtualMachineConfig.java)
+builder.
+
+Inside the VM this storage is mounted at a path that can be retrieved via the
+[`AVmPayload_getEncryptedStoragePath()`](../vm_payload/include/vm_payload.h)
+function. The VM can create sub-directories and read and write files here. Any
+data written is persisted and should be available next time the VM is run. (An
+automatic sync is done when the payload exits normally.)
+
+Outside the VM the storage is persisted as a file in the app’s private data
+directory. The data is encrypted using a key derived from the VM secret, which
+is not made available outside the VM.
+
+So an attacker should not be able to decrypt the data; however, a sufficiently
+powerful attacker could delete it, wholly or partially roll it back to an
+earlier version, or modify it, corrupting the data.
+
+# Transferring a VM
+
+It is possible to make a copy of a VM instance. This can be used to transfer a
+VM from one app to another, which can be useful in some circumstances.
+
+This should only be done while the VM is stopped. The first step is to call
+`toDescriptor()` on the
+[`VirtualMachine`](src/android/system/virtualmachine/VirtualMachine.java)
+instance, which returns a
+[`VirtualMachineDescriptor`](src/android/system/virtualmachine/VirtualMachineDescriptor.java)
+object. This object internally contains open file descriptors to the files that
+hold the VM's state (its instance data, configuration, and encrypted storage).
+
+A `VirtualMachineDescriptor` is
+[`Parcelable`](https://developer.android.com/reference/android/os/Parcelable),
+so it can be passed to another app via a Binder call.  Any app with a
+`VirtualMachineDescriptor` can pass it, along with a new VM name, to the
+`importFromDescriptor()` method on
+[`VirtualMachineManager`](src/android/system/virtualmachine/VirtualMachineManager.java). This
+is equivalent to calling `create()` with the same name and configuration, except
+that the new VM is the same instance as the original, with the same VM secret,
+and has access to a copy of the original's encrypted storage.
+
+Once the transfer has been completed it would be reasonable to delete the
+original VM, using the `delete()` method on `VirtualMachineManager`.
+
+
+
+
+
diff --git a/libs/libfdt/src/lib.rs b/libs/libfdt/src/lib.rs
index 96ac3f4..b369390 100644
--- a/libs/libfdt/src/lib.rs
+++ b/libs/libfdt/src/lib.rs
@@ -33,6 +33,13 @@
 use core::result;
 use zerocopy::AsBytes as _;
 
+// TODO(b/308694211): Use cstr!() from vmbase
+macro_rules! cstr {
+    ($str:literal) => {{
+        core::ffi::CStr::from_bytes_with_nul(concat!($str, "\0").as_bytes()).unwrap()
+    }};
+}
+
 /// Error type corresponding to libfdt error codes.
 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
 pub enum FdtError {
@@ -297,12 +304,12 @@
 
     /// Returns the standard (deprecated) device_type <string> property.
     pub fn device_type(&self) -> Result<Option<&CStr>> {
-        self.getprop_str(CStr::from_bytes_with_nul(b"device_type\0").unwrap())
+        self.getprop_str(cstr!("device_type"))
     }
 
     /// Returns the standard reg <prop-encoded-array> property.
     pub fn reg(&self) -> Result<Option<RegIterator<'a>>> {
-        let reg = CStr::from_bytes_with_nul(b"reg\0").unwrap();
+        let reg = cstr!("reg");
 
         if let Some(cells) = self.getprop_cells(reg)? {
             let parent = self.parent()?;
@@ -318,7 +325,7 @@
 
     /// Returns the standard ranges property.
     pub fn ranges<A, P, S>(&self) -> Result<Option<RangesIterator<'a, A, P, S>>> {
-        let ranges = CStr::from_bytes_with_nul(b"ranges\0").unwrap();
+        let ranges = cstr!("ranges");
         if let Some(cells) = self.getprop_cells(ranges)? {
             let parent = self.parent()?;
             let addr_cells = self.address_cells()?;
@@ -497,6 +504,18 @@
 
         fdt_err_or_option(ret)?.map(|offset| FdtProperty::new(self.fdt, offset)).transpose()
     }
+
+    /// Returns the phandle
+    pub fn get_phandle(&self) -> Result<Option<Phandle>> {
+        // This rewrites the fdt_get_phandle() because it doesn't return error code.
+        if let Some(prop) = self.getprop_u32(cstr!("phandle"))? {
+            Ok(Some(prop.try_into()?))
+        } else if let Some(prop) = self.getprop_u32(cstr!("linux,phandle"))? {
+            Ok(Some(prop.try_into()?))
+        } else {
+            Ok(None)
+        }
+    }
 }
 
 impl<'a> PartialEq for FdtNode<'a> {
@@ -689,6 +708,11 @@
         self.fdt
     }
 
+    /// Returns immutable FdtNode of this node.
+    pub fn as_node(&self) -> FdtNode {
+        FdtNode { fdt: self.fdt, offset: self.offset }
+    }
+
     /// Adds 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> {
         let offset = self.add_subnode_offset(name.to_bytes())?;
@@ -938,8 +962,8 @@
     ///
     /// NOTE: This does not support individual "/memory@XXXX" banks.
     pub fn memory(&self) -> Result<MemRegIterator> {
-        let memory_node_name = CStr::from_bytes_with_nul(b"/memory\0").unwrap();
-        let memory_device_type = CStr::from_bytes_with_nul(b"memory\0").unwrap();
+        let memory_node_name = cstr!("/memory");
+        let memory_device_type = cstr!("memory");
 
         let node = self.node(memory_node_name)?.ok_or(FdtError::NotFound)?;
         if node.device_type()? != Some(memory_device_type) {
@@ -955,17 +979,27 @@
 
     /// Returns the standard /chosen node.
     pub fn chosen(&self) -> Result<Option<FdtNode>> {
-        self.node(CStr::from_bytes_with_nul(b"/chosen\0").unwrap())
+        self.node(cstr!("/chosen"))
     }
 
     /// Returns the standard /chosen node as mutable.
     pub fn chosen_mut(&mut self) -> Result<Option<FdtNodeMut>> {
-        self.node_mut(CStr::from_bytes_with_nul(b"/chosen\0").unwrap())
+        self.node_mut(cstr!("/chosen"))
     }
 
     /// Returns the root node of the tree.
     pub fn root(&self) -> Result<FdtNode> {
-        self.node(CStr::from_bytes_with_nul(b"/\0").unwrap())?.ok_or(FdtError::Internal)
+        self.node(cstr!("/"))?.ok_or(FdtError::Internal)
+    }
+
+    /// Returns the standard /__symbols__ node.
+    pub fn symbols(&self) -> Result<Option<FdtNode>> {
+        self.node(cstr!("/__symbols__"))
+    }
+
+    /// Returns the standard /__symbols__ node as mutable
+    pub fn symbols_mut(&mut self) -> Result<Option<FdtNodeMut>> {
+        self.node_mut(cstr!("/__symbols__"))
     }
 
     /// Returns a tree node by its full path.
@@ -990,14 +1024,25 @@
 
     /// Returns a node with the phandle
     pub fn node_with_phandle(&self, phandle: Phandle) -> Result<Option<FdtNode>> {
-        // SAFETY: Accesses (read-only) are constrained to the DT totalsize.
+        let offset = self.node_offset_with_phandle(phandle)?;
+        Ok(offset.map(|offset| FdtNode { fdt: self, offset }))
+    }
+
+    /// Returns a mutable node with the phandle
+    pub fn node_mut_with_phandle(&mut self, phandle: Phandle) -> Result<Option<FdtNodeMut>> {
+        let offset = self.node_offset_with_phandle(phandle)?;
+        Ok(offset.map(|offset| FdtNodeMut { fdt: self, offset }))
+    }
+
+    fn node_offset_with_phandle(&self, phandle: Phandle) -> Result<Option<c_int>> {
+        // SAFETY: Accesses are constrained to the DT totalsize.
         let ret = unsafe { libfdt_bindgen::fdt_node_offset_by_phandle(self.as_ptr(), phandle.0) };
-        Ok(fdt_err_or_option(ret)?.map(|offset| FdtNode { fdt: self, offset }))
+        fdt_err_or_option(ret)
     }
 
     /// Returns 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)
+        self.node_mut(cstr!("/"))?.ok_or(FdtError::Internal)
     }
 
     /// Returns a mutable tree node by its full path.
diff --git a/libs/libfdt/tests/api_test.rs b/libs/libfdt/tests/api_test.rs
index 78bb618..d76b1a4 100644
--- a/libs/libfdt/tests/api_test.rs
+++ b/libs/libfdt/tests/api_test.rs
@@ -16,14 +16,21 @@
 
 //! Integration tests of the library libfdt.
 
-use libfdt::{Fdt, FdtError, Phandle};
-use std::ffi::{CStr, CString};
+use core::ffi::CStr;
+use libfdt::{Fdt, FdtError, FdtNodeMut, Phandle};
+use std::ffi::CString;
 use std::fs;
 use std::ops::Range;
 
+// TODO(b/308694211): Use cstr!() from vmbase
 macro_rules! cstr {
     ($str:literal) => {{
-        CStr::from_bytes_with_nul(concat!($str, "\0").as_bytes()).unwrap()
+        const S: &str = concat!($str, "\0");
+        const C: &::core::ffi::CStr = match ::core::ffi::CStr::from_bytes_with_nul(S.as_bytes()) {
+            Ok(v) => v,
+            Err(_) => panic!("string contains interior NUL"),
+        };
+        C
     }};
 }
 
@@ -100,11 +107,11 @@
     let data = fs::read(TEST_TREE_WITH_NO_MEMORY_NODE_PATH).unwrap();
     let fdt = Fdt::from_slice(&data).unwrap();
     let root = fdt.root().unwrap();
-    let expected = [cstr!("cpus"), cstr!("randomnode"), cstr!("chosen")];
+    let expected = [Ok(cstr!("cpus")), Ok(cstr!("randomnode")), Ok(cstr!("chosen"))];
 
-    for (node, name) in root.subnodes().unwrap().zip(expected) {
-        assert_eq!(node.name(), Ok(name));
-    }
+    let root_subnodes = root.subnodes().unwrap();
+    let subnode_names: Vec<_> = root_subnodes.map(|node| node.name()).collect();
+    assert_eq!(subnode_names, expected);
 }
 
 #[test]
@@ -113,18 +120,19 @@
     let fdt = Fdt::from_slice(&data).unwrap();
     let root = fdt.root().unwrap();
     let one_be = 0x1_u32.to_be_bytes();
-    let expected = [
-        (cstr!("model"), b"MyBoardName\0".as_ref()),
-        (cstr!("compatible"), b"MyBoardName\0MyBoardFamilyName\0".as_ref()),
-        (cstr!("#address-cells"), &one_be),
-        (cstr!("#size-cells"), &one_be),
-        (cstr!("empty_prop"), &[]),
+    type Result<T> = core::result::Result<T, FdtError>;
+    let expected: Vec<(Result<&CStr>, Result<&[u8]>)> = vec![
+        (Ok(cstr!("model")), Ok(b"MyBoardName\0".as_ref())),
+        (Ok(cstr!("compatible")), Ok(b"MyBoardName\0MyBoardFamilyName\0".as_ref())),
+        (Ok(cstr!("#address-cells")), Ok(&one_be)),
+        (Ok(cstr!("#size-cells")), Ok(&one_be)),
+        (Ok(cstr!("empty_prop")), Ok(&[])),
     ];
 
     let properties = root.properties().unwrap();
-    for (prop, (name, value)) in properties.zip(expected.into_iter()) {
-        assert_eq!((prop.name(), prop.value()), (Ok(name), Ok(value)));
-    }
+    let subnode_properties: Vec<_> = properties.map(|prop| (prop.name(), prop.value())).collect();
+
+    assert_eq!(subnode_properties, expected);
 }
 
 #[test]
@@ -132,12 +140,16 @@
     let data = fs::read(TEST_TREE_WITH_NO_MEMORY_NODE_PATH).unwrap();
     let fdt = Fdt::from_slice(&data).unwrap();
     let node = fdt.node(cstr!("/cpus/PowerPC,970@1")).unwrap().unwrap();
-    let expected = [cstr!(""), cstr!("cpus"), cstr!("PowerPC,970@1")];
+    let expected = vec![Ok(cstr!("")), Ok(cstr!("cpus")), Ok(cstr!("PowerPC,970@1"))];
 
-    for (depth, name) in expected.into_iter().enumerate() {
-        let supernode = node.supernode_at_depth(depth).unwrap();
-        assert_eq!(supernode.name(), Ok(name));
+    let mut supernode_names = vec![];
+    let mut depth = 0;
+    while let Ok(supernode) = node.supernode_at_depth(depth) {
+        supernode_names.push(supernode.name());
+        depth += 1;
     }
+
+    assert_eq!(supernode_names, expected);
 }
 
 #[test]
@@ -194,6 +206,40 @@
 }
 
 #[test]
+fn node_mut_with_phandle() {
+    let mut data = fs::read(TEST_TREE_PHANDLE_PATH).unwrap();
+    let fdt = Fdt::from_mut_slice(&mut data).unwrap();
+
+    // Test linux,phandle
+    let phandle = Phandle::new(0xFF).unwrap();
+    let node: FdtNodeMut = fdt.node_mut_with_phandle(phandle).unwrap().unwrap();
+    assert_eq!(node.as_node().name(), Ok(cstr!("node_zz")));
+
+    // Test phandle
+    let phandle = Phandle::new(0x22).unwrap();
+    let node: FdtNodeMut = fdt.node_mut_with_phandle(phandle).unwrap().unwrap();
+    assert_eq!(node.as_node().name(), Ok(cstr!("node_abc")));
+}
+
+#[test]
+fn node_get_phandle() {
+    let data = fs::read(TEST_TREE_PHANDLE_PATH).unwrap();
+    let fdt = Fdt::from_slice(&data).unwrap();
+
+    // Test linux,phandle
+    let node = fdt.node(cstr!("/node_z/node_zz")).unwrap().unwrap();
+    assert_eq!(node.get_phandle(), Ok(Phandle::new(0xFF)));
+
+    // Test phandle
+    let node = fdt.node(cstr!("/node_a/node_ab/node_abc")).unwrap().unwrap();
+    assert_eq!(node.get_phandle(), Ok(Phandle::new(0x22)));
+
+    // Test no phandle
+    let node = fdt.node(cstr!("/node_b")).unwrap().unwrap();
+    assert_eq!(node.get_phandle(), Ok(None));
+}
+
+#[test]
 fn node_nop() {
     let mut data = fs::read(TEST_TREE_PHANDLE_PATH).unwrap();
     let fdt = Fdt::from_mut_slice(&mut data).unwrap();
@@ -246,3 +292,30 @@
         assert_eq!(subnode.name(), Ok(name.as_c_str()));
     }
 }
+
+#[test]
+fn fdt_symbols() {
+    let mut data = fs::read(TEST_TREE_PHANDLE_PATH).unwrap();
+    let fdt = Fdt::from_mut_slice(&mut data).unwrap();
+
+    let symbols = fdt.symbols().unwrap().unwrap();
+    assert_eq!(symbols.name(), Ok(cstr!("__symbols__")));
+
+    // Validates type.
+    let _symbols: FdtNodeMut = fdt.symbols_mut().unwrap().unwrap();
+}
+
+#[test]
+fn node_mut_as_node() {
+    let mut data = fs::read(TEST_TREE_WITH_ONE_MEMORY_RANGE_PATH).unwrap();
+    let fdt = Fdt::from_mut_slice(&mut data).unwrap();
+
+    let mut memory = fdt.node_mut(cstr!("/memory")).unwrap().unwrap();
+    {
+        let memory = memory.as_node();
+        assert_eq!(memory.name(), Ok(cstr!("memory")));
+    }
+
+    // Just check whether borrow checker doesn't complain this.
+    memory.setprop_inplace(cstr!("device_type"), b"MEMORY\0").unwrap();
+}
diff --git a/libs/libfdt/tests/data/test_tree_phandle.dts b/libs/libfdt/tests/data/test_tree_phandle.dts
index 0438241..97d9028 100644
--- a/libs/libfdt/tests/data/test_tree_phandle.dts
+++ b/libs/libfdt/tests/data/test_tree_phandle.dts
@@ -32,4 +32,8 @@
             };
         };
     };
+
+    __symbols__ {
+        symbol_a = "/node_a";
+    };
 };
\ No newline at end of file
diff --git a/microdroid/init.rc b/microdroid/init.rc
index c257cdb..4cc0475 100644
--- a/microdroid/init.rc
+++ b/microdroid/init.rc
@@ -30,8 +30,13 @@
     # We don't directly exec the binary to specify stdio_to_kmsg.
     exec_start init_debug_policy
 
+    # Wait for ueventd to have finished cold boot.
+    # This is needed by prng-seeder (at least).
+    # (In Android this happens inside apexd-bootstrap.)
+    wait_for_prop ro.cold_boot_done true
+
 on init
-    mkdir /mnt/apk 0755 system system
+    mkdir /mnt/apk 0755 root root
     mkdir /mnt/extra-apk 0755 root root
 
     # Allow the payload access to the console (default is 0600)
@@ -40,9 +45,6 @@
     # Microdroid_manager starts apkdmverity/zipfuse/apexd
     start microdroid_manager
 
-    # restorecon so microdroid_manager can create subdirectories
-    restorecon /mnt/extra-apk
-
     # Wait for apexd to finish activating APEXes before starting more processes.
     # Microdroid starts apexd in VM mode in which apexd doesn't wait for init after setting
     # apexd.status to activated, but immediately transitions to ready. Therefore, it's not safe to
diff --git a/microdroid_manager/Android.bp b/microdroid_manager/Android.bp
index 8710e54..93f49ef 100644
--- a/microdroid_manager/Android.bp
+++ b/microdroid_manager/Android.bp
@@ -46,10 +46,12 @@
         "libserde",
         "libserde_cbor",
         "libserde_json",
+        "libservice_vm_comm",
         "libthiserror",
         "libuuid",
         "libvsock",
         "librand",
+        "libzeroize",
     ],
     init_rc: ["microdroid_manager.rc"],
     multilib: {
@@ -70,6 +72,7 @@
     defaults: ["microdroid_manager_defaults"],
     test_suites: ["general-tests"],
     rustlibs: [
+        "libhwtrust",
         "libtempfile",
     ],
     multilib: {
diff --git a/microdroid_manager/aidl/Android.bp b/microdroid_manager/aidl/Android.bp
index 0aa8662..353e9cc 100644
--- a/microdroid_manager/aidl/Android.bp
+++ b/microdroid_manager/aidl/Android.bp
@@ -5,8 +5,12 @@
 aidl_interface {
     name: "android.system.virtualization.payload",
     srcs: ["android/system/virtualization/payload/*.aidl"],
+    imports: ["android.system.virtualizationcommon"],
     unstable: true,
     backend: {
+        java: {
+            enabled: false,
+        },
         rust: {
             enabled: true,
             apex_available: [
diff --git a/microdroid_manager/aidl/android/system/virtualization/payload/IVmPayloadService.aidl b/microdroid_manager/aidl/android/system/virtualization/payload/IVmPayloadService.aidl
index b9a7a64..51796f1 100644
--- a/microdroid_manager/aidl/android/system/virtualization/payload/IVmPayloadService.aidl
+++ b/microdroid_manager/aidl/android/system/virtualization/payload/IVmPayloadService.aidl
@@ -16,11 +16,17 @@
 
 package android.system.virtualization.payload;
 
+import android.system.virtualizationcommon.Certificate;
+
 /**
  * This interface regroups the tasks that payloads delegate to
  * Microdroid Manager for execution.
  */
 interface IVmPayloadService {
+    /** The constants STATUS_* are status code returned by this service. */
+    /** Failed to prepare the CSR and key pair for attestation. */
+    const int STATUS_FAILED_TO_PREPARE_CSR_AND_KEY = 1;
+
     /** Socket name of the service IVmPayloadService. */
     const String VM_PAYLOAD_SERVICE_SOCKET_NAME = "vm_payload_service";
 
@@ -33,6 +39,32 @@
      */
     const String ENCRYPTEDSTORE_MOUNTPOINT = "/mnt/encryptedstore";
 
+    /**
+     * An {@link AttestationResult} holds an attested private key and the remotely
+     * provisioned certificate chain covering its corresponding public key.
+     */
+    parcelable AttestationResult {
+        /**
+         * DER-encoded ECPrivateKey structure specified in [RFC 5915 s3] for the
+         * EC P-256 private key, which is attested.
+         *
+         * The corresponding public key is included in the leaf certificate of
+         * the certificate chain.
+         *
+         * [RFC 5915 s3]: https://datatracker.ietf.org/doc/html/rfc5915#section-3
+         */
+        byte[] privateKey;
+
+        /**
+         * Sequence of DER-encoded X.509 certificates that make up the attestation
+         * key's certificate chain.
+         *
+         * The certificate chain starts with a root certificate and ends with a leaf
+         * certificate covering the attested public key.
+         */
+        Certificate[] certificateChain;
+    }
+
     /** Notifies that the payload is ready to serve. */
     void notifyPayloadReady();
 
@@ -75,7 +107,9 @@
      * serving as proof of the freshness of the result.
      *
      * @param challenge the maximum supported challenge size is 64 bytes.
-     * @return the X.509 encoded certificate.
+     *
+     * @return An {@link AttestationResult} parcelable containing an attested key pair and its
+     *         certification chain.
      */
-    byte[] requestAttestation(in byte[] challenge);
+    AttestationResult requestAttestation(in byte[] challenge);
 }
diff --git a/microdroid_manager/src/main.rs b/microdroid_manager/src/main.rs
index 7ba54f8..1b41e58 100644
--- a/microdroid_manager/src/main.rs
+++ b/microdroid_manager/src/main.rs
@@ -34,7 +34,7 @@
 
 use crate::dice::dice_derivation;
 use crate::dice_driver::DiceDriver;
-use crate::instance::{ApexData, InstanceDisk, MicrodroidData};
+use crate::instance::{InstanceDisk, MicrodroidData};
 use crate::verify::verify_payload;
 use crate::vm_payload_service::register_vm_payload_service;
 use anyhow::{anyhow, bail, ensure, Context, Error, Result};
@@ -42,10 +42,10 @@
 use keystore2_crypto::ZVec;
 use libc::VMADDR_CID_HOST;
 use log::{error, info};
-use microdroid_metadata::{write_metadata, PayloadMetadata};
+use microdroid_metadata::PayloadMetadata;
 use microdroid_payload_config::{OsConfig, Task, TaskType, VmPayloadConfig};
 use nix::sys::signal::Signal;
-use payload::{load_metadata, to_metadata};
+use payload::load_metadata;
 use rpcbinder::RpcSession;
 use rustutils::sockets::android_get_control_socket;
 use rustutils::system_properties;
@@ -143,15 +143,6 @@
     Ok(())
 }
 
-fn get_vms_rpc_binder() -> Result<Strong<dyn IVirtualMachineService>> {
-    // The host is running a VirtualMachineService for this VM on a port equal
-    // to the CID of this VM.
-    let port = vsock::get_local_cid().context("Could not determine local CID")?;
-    RpcSession::new()
-        .setup_vsock_client(VMADDR_CID_HOST, port)
-        .context("Could not connect to IVirtualMachineService")
-}
-
 fn main() -> Result<()> {
     // If debuggable, print full backtrace to console log with stdio_to_kmsg
     if is_debuggable()? {
@@ -174,25 +165,6 @@
     })
 }
 
-/// Prepares a socket file descriptor for the vm payload service.
-///
-/// # Safety
-///
-/// The caller must ensure that this function is the only place that claims ownership
-/// of the file descriptor and it is called only once.
-unsafe fn prepare_vm_payload_service_socket() -> Result<OwnedFd> {
-    let raw_fd = android_get_control_socket(VM_PAYLOAD_SERVICE_SOCKET_NAME)?;
-
-    // Creating OwnedFd for stdio FDs is not safe.
-    if [libc::STDIN_FILENO, libc::STDOUT_FILENO, libc::STDERR_FILENO].contains(&raw_fd) {
-        bail!("File descriptor {raw_fd} is standard I/O descriptor");
-    }
-    // SAFETY: Initializing OwnedFd for a RawFd created by the init.
-    // We checked that the integer value corresponds to a valid FD and that the caller
-    // ensures that this is the only place to claim its ownership.
-    Ok(unsafe { OwnedFd::from_raw_fd(raw_fd) })
-}
-
 fn try_main() -> Result<()> {
     android_logger::init_once(
         android_logger::Config::default()
@@ -245,71 +217,6 @@
     }
 }
 
-fn post_payload_work() -> Result<()> {
-    // Sync the encrypted storage filesystem (flushes the filesystem caches).
-    if Path::new(ENCRYPTEDSTORE_BACKING_DEVICE).exists() {
-        let mountpoint = CString::new(ENCRYPTEDSTORE_MOUNTPOINT).unwrap();
-
-        // SAFETY: `mountpoint` is a valid C string. `syncfs` and `close` are safe for any parameter
-        // values.
-        let ret = unsafe {
-            let dirfd = libc::open(
-                mountpoint.as_ptr(),
-                libc::O_DIRECTORY | libc::O_RDONLY | libc::O_CLOEXEC,
-            );
-            ensure!(dirfd >= 0, "Unable to open {:?}", mountpoint);
-            let ret = libc::syncfs(dirfd);
-            libc::close(dirfd);
-            ret
-        };
-        if ret != 0 {
-            error!("failed to sync encrypted storage.");
-            return Err(anyhow!(std::io::Error::last_os_error()));
-        }
-    }
-    Ok(())
-}
-
-fn is_strict_boot() -> bool {
-    Path::new(AVF_STRICT_BOOT).exists()
-}
-
-fn is_new_instance() -> bool {
-    Path::new(AVF_NEW_INSTANCE).exists()
-}
-
-fn is_verified_boot() -> bool {
-    !Path::new(DEBUG_MICRODROID_NO_VERIFIED_BOOT).exists()
-}
-
-fn is_debuggable() -> Result<bool> {
-    Ok(system_properties::read_bool(DEBUGGABLE_PROP, true)?)
-}
-
-fn should_export_tombstones(config: &VmPayloadConfig) -> bool {
-    match config.export_tombstones {
-        Some(b) => b,
-        None => is_debuggable().unwrap_or(false),
-    }
-}
-
-/// Get debug policy value in bool. It's true iff the value is explicitly set to <1>.
-fn get_debug_policy_bool(path: &'static str) -> Result<Option<bool>> {
-    let mut file = match File::open(path) {
-        Ok(dp) => dp,
-        Err(e) => {
-            info!(
-                "Assumes that debug policy is disabled because failed to read debug policy ({e:?})"
-            );
-            return Ok(Some(false));
-        }
-    };
-    let mut log: [u8; 4] = Default::default();
-    file.read_exact(&mut log).context("Malformed data in {path}")?;
-    // DT spec uses big endian although Android is always little endian.
-    Ok(Some(u32::from_be_bytes(log) == 1))
-}
-
 fn try_run_payload(
     service: &Strong<dyn IVirtualMachineService>,
     vm_payload_service_fd: OwnedFd,
@@ -377,6 +284,13 @@
     let dice_artifacts = dice_derivation(dice, &verified_data, &payload_metadata)?;
     let vm_secret = VmSecret::new(dice_artifacts).context("Failed to create VM secrets")?;
 
+    if cfg!(dice_changes) {
+        // Now that the DICE derivation is done, it's ok to allow payload code to run.
+
+        // Start apexd to activate APEXes. This may allow code within them to run.
+        system_properties::write("ctl.start", "apexd-vm")?;
+    }
+
     // Run encryptedstore binary to prepare the storage
     let encryptedstore_child = if Path::new(ENCRYPTEDSTORE_BACKING_DEVICE).exists() {
         info!("Preparing encryptedstore ...");
@@ -419,10 +333,12 @@
     );
     mount_extra_apks(&config, &mut zipfuse)?;
 
-    // Wait until apex config is done. (e.g. linker configuration for apexes)
-    wait_for_apex_config_done()?;
-
-    setup_config_sysprops(&config)?;
+    register_vm_payload_service(
+        allow_restricted_apis,
+        service.clone(),
+        vm_secret,
+        vm_payload_service_fd,
+    )?;
 
     // Set export_tombstones if enabled
     if should_export_tombstones(&config) {
@@ -431,16 +347,20 @@
             .context("set microdroid_manager.export_tombstones.enabled")?;
     }
 
+    // Wait until apex config is done. (e.g. linker configuration for apexes)
+    wait_for_property_true(APEX_CONFIG_DONE_PROP).context("Failed waiting for apex config done")?;
+
+    // Trigger init post-fs-data. This will start authfs if we wask it to.
+    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")?;
+
     // Wait until zipfuse has mounted the APKs so we can access the payload
     zipfuse.wait_until_done()?;
 
-    register_vm_payload_service(
-        allow_restricted_apis,
-        service.clone(),
-        vm_secret,
-        vm_payload_service_fd,
-    )?;
-
     // Wait for encryptedstore to finish mounting the storage (if enabled) before setting
     // microdroid_manager.init_done. Reason is init stops uneventd after that.
     // Encryptedstore, however requires ueventd
@@ -449,7 +369,10 @@
         ensure!(exitcode.success(), "Unable to prepare encrypted storage. Exitcode={}", exitcode);
     }
 
+    // Wait for init to have finished booting.
     wait_for_property_true("dev.bootcomplete").context("failed waiting for dev.bootcomplete")?;
+
+    // And then tell it we're done so unnecessary services can be shut down.
     system_properties::write("microdroid_manager.init_done", "1")
         .context("set microdroid_manager.init_done")?;
 
@@ -457,6 +380,120 @@
     exec_task(task, service).context("Failed to run payload")
 }
 
+fn post_payload_work() -> Result<()> {
+    // Sync the encrypted storage filesystem (flushes the filesystem caches).
+    if Path::new(ENCRYPTEDSTORE_BACKING_DEVICE).exists() {
+        let mountpoint = CString::new(ENCRYPTEDSTORE_MOUNTPOINT).unwrap();
+
+        // SAFETY: `mountpoint` is a valid C string. `syncfs` and `close` are safe for any parameter
+        // values.
+        let ret = unsafe {
+            let dirfd = libc::open(
+                mountpoint.as_ptr(),
+                libc::O_DIRECTORY | libc::O_RDONLY | libc::O_CLOEXEC,
+            );
+            ensure!(dirfd >= 0, "Unable to open {:?}", mountpoint);
+            let ret = libc::syncfs(dirfd);
+            libc::close(dirfd);
+            ret
+        };
+        if ret != 0 {
+            error!("failed to sync encrypted storage.");
+            return Err(anyhow!(std::io::Error::last_os_error()));
+        }
+    }
+    Ok(())
+}
+
+fn mount_extra_apks(config: &VmPayloadConfig, zipfuse: &mut Zipfuse) -> Result<()> {
+    // For now, only the number of apks is important, as the mount point and dm-verity name is fixed
+    for i in 0..config.extra_apks.len() {
+        let mount_dir = format!("/mnt/extra-apk/{i}");
+        create_dir(Path::new(&mount_dir)).context("Failed to create mount dir for extra apks")?;
+
+        let mount_for_exec =
+            if cfg!(multi_tenant) { MountForExec::Allowed } else { MountForExec::Disallowed };
+        // These run asynchronously in parallel - we wait later for them to complete.
+        zipfuse.mount(
+            mount_for_exec,
+            "fscontext=u:object_r:zipfusefs:s0,context=u:object_r:extra_apk_file:s0",
+            Path::new(&format!("/dev/block/mapper/extra-apk-{i}")),
+            Path::new(&mount_dir),
+            format!("microdroid_manager.extra_apk.mounted.{i}"),
+        )?;
+    }
+
+    Ok(())
+}
+
+fn get_vms_rpc_binder() -> Result<Strong<dyn IVirtualMachineService>> {
+    // The host is running a VirtualMachineService for this VM on a port equal
+    // to the CID of this VM.
+    let port = vsock::get_local_cid().context("Could not determine local CID")?;
+    RpcSession::new()
+        .setup_vsock_client(VMADDR_CID_HOST, port)
+        .context("Could not connect to IVirtualMachineService")
+}
+
+/// Prepares a socket file descriptor for the vm payload service.
+///
+/// # Safety
+///
+/// The caller must ensure that this function is the only place that claims ownership
+/// of the file descriptor and it is called only once.
+unsafe fn prepare_vm_payload_service_socket() -> Result<OwnedFd> {
+    let raw_fd = android_get_control_socket(VM_PAYLOAD_SERVICE_SOCKET_NAME)?;
+
+    // Creating OwnedFd for stdio FDs is not safe.
+    if [libc::STDIN_FILENO, libc::STDOUT_FILENO, libc::STDERR_FILENO].contains(&raw_fd) {
+        bail!("File descriptor {raw_fd} is standard I/O descriptor");
+    }
+    // SAFETY: Initializing OwnedFd for a RawFd created by the init.
+    // We checked that the integer value corresponds to a valid FD and that the caller
+    // ensures that this is the only place to claim its ownership.
+    Ok(unsafe { OwnedFd::from_raw_fd(raw_fd) })
+}
+
+fn is_strict_boot() -> bool {
+    Path::new(AVF_STRICT_BOOT).exists()
+}
+
+fn is_new_instance() -> bool {
+    Path::new(AVF_NEW_INSTANCE).exists()
+}
+
+fn is_verified_boot() -> bool {
+    !Path::new(DEBUG_MICRODROID_NO_VERIFIED_BOOT).exists()
+}
+
+fn is_debuggable() -> Result<bool> {
+    Ok(system_properties::read_bool(DEBUGGABLE_PROP, true)?)
+}
+
+fn should_export_tombstones(config: &VmPayloadConfig) -> bool {
+    match config.export_tombstones {
+        Some(b) => b,
+        None => is_debuggable().unwrap_or(false),
+    }
+}
+
+/// Get debug policy value in bool. It's true iff the value is explicitly set to <1>.
+fn get_debug_policy_bool(path: &'static str) -> Result<Option<bool>> {
+    let mut file = match File::open(path) {
+        Ok(dp) => dp,
+        Err(e) => {
+            info!(
+                "Assumes that debug policy is disabled because failed to read debug policy ({e:?})"
+            );
+            return Ok(Some(false));
+        }
+    };
+    let mut log: [u8; 4] = Default::default();
+    file.read_exact(&mut log).context("Malformed data in {path}")?;
+    // DT spec uses big endian although Android is always little endian.
+    Ok(Some(u32::from_be_bytes(log) == 1))
+}
+
 enum MountForExec {
     Allowed,
     Disallowed,
@@ -504,65 +541,6 @@
     }
 }
 
-fn write_apex_payload_data(
-    saved_data: Option<&MicrodroidData>,
-    apex_data_from_payload: &[ApexData],
-) -> Result<()> {
-    if let Some(saved_apex_data) = saved_data.map(|d| &d.apex_data) {
-        // We don't support APEX updates. (assuming that update will change root digest)
-        ensure!(
-            saved_apex_data == apex_data_from_payload,
-            MicrodroidError::PayloadChanged(String::from("APEXes have changed."))
-        );
-        let apex_metadata = to_metadata(apex_data_from_payload);
-        // Pass metadata(with public keys and root digests) to apexd so that it uses the passed
-        // metadata instead of the default one (/dev/block/by-name/payload-metadata)
-        OpenOptions::new()
-            .create_new(true)
-            .write(true)
-            .open("/apex/vm-payload-metadata")
-            .context("Failed to open /apex/vm-payload-metadata")
-            .and_then(|f| write_metadata(&apex_metadata, f))?;
-    }
-    Ok(())
-}
-
-fn mount_extra_apks(config: &VmPayloadConfig, zipfuse: &mut Zipfuse) -> Result<()> {
-    // For now, only the number of apks is important, as the mount point and dm-verity name is fixed
-    for i in 0..config.extra_apks.len() {
-        let mount_dir = format!("/mnt/extra-apk/{i}");
-        create_dir(Path::new(&mount_dir)).context("Failed to create mount dir for extra apks")?;
-
-        let mount_for_exec =
-            if cfg!(multi_tenant) { MountForExec::Allowed } else { MountForExec::Disallowed };
-        // These run asynchronously in parallel - we wait later for them to complete.
-        zipfuse.mount(
-            mount_for_exec,
-            "fscontext=u:object_r:zipfusefs:s0,context=u:object_r:extra_apk_file:s0",
-            Path::new(&format!("/dev/block/mapper/extra-apk-{i}")),
-            Path::new(&mount_dir),
-            format!("microdroid_manager.extra_apk.mounted.{i}"),
-        )?;
-    }
-
-    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")
-}
-
 fn wait_for_property_true(property_name: &str) -> Result<()> {
     let mut prop = PropertyWatcher::new(property_name)?;
     loop {
diff --git a/microdroid_manager/src/verify.rs b/microdroid_manager/src/verify.rs
index 22f3414..e63530b 100644
--- a/microdroid_manager/src/verify.rs
+++ b/microdroid_manager/src/verify.rs
@@ -12,18 +12,19 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-use crate::instance::{ApkData, MicrodroidData, RootHash};
-use crate::payload::get_apex_data_from_payload;
-use crate::{is_strict_boot, is_verified_boot, write_apex_payload_data, MicrodroidError};
+use crate::instance::{ApexData, ApkData, MicrodroidData, RootHash};
+use crate::payload::{get_apex_data_from_payload, to_metadata};
+use crate::{is_strict_boot, is_verified_boot, MicrodroidError};
 use anyhow::{anyhow, ensure, Context, Result};
 use apkmanifest::get_manifest_info;
 use apkverify::{get_public_key_der, verify, V4Signature};
 use glob::glob;
 use itertools::sorted;
 use log::{info, warn};
-use microdroid_metadata::Metadata;
+use microdroid_metadata::{write_metadata, Metadata};
 use rand::Fill;
 use rustutils::system_properties;
+use std::fs::OpenOptions;
 use std::path::Path;
 use std::process::{Child, Command};
 use std::str;
@@ -134,8 +135,10 @@
         write_apex_payload_data(saved_data, &apex_data_from_payload)?;
     }
 
-    // Start apexd to activate APEXes
-    system_properties::write("ctl.start", "apexd-vm")?;
+    if cfg!(not(dice_changes)) {
+        // Start apexd to activate APEXes
+        system_properties::write("ctl.start", "apexd-vm")?;
+    }
 
     // TODO(inseob): add timeout
     apkdmverity_child.wait()?;
@@ -207,6 +210,29 @@
     })
 }
 
+fn write_apex_payload_data(
+    saved_data: Option<&MicrodroidData>,
+    apex_data_from_payload: &[ApexData],
+) -> Result<()> {
+    if let Some(saved_apex_data) = saved_data.map(|d| &d.apex_data) {
+        // We don't support APEX updates. (assuming that update will change root digest)
+        ensure!(
+            saved_apex_data == apex_data_from_payload,
+            MicrodroidError::PayloadChanged(String::from("APEXes have changed."))
+        );
+        let apex_metadata = to_metadata(apex_data_from_payload);
+        // Pass metadata(with public keys and root digests) to apexd so that it uses the passed
+        // metadata instead of the default one (/dev/block/by-name/payload-metadata)
+        OpenOptions::new()
+            .create_new(true)
+            .write(true)
+            .open("/apex/vm-payload-metadata")
+            .context("Failed to open /apex/vm-payload-metadata")
+            .and_then(|f| write_metadata(&apex_metadata, f))?;
+    }
+    Ok(())
+}
+
 fn get_apk_root_hash_from_idsig<P: AsRef<Path>>(idsig_path: P) -> Result<Box<RootHash>> {
     Ok(V4Signature::from_idsig_path(idsig_path)?.hashing_info.raw_root_hash)
 }
diff --git a/microdroid_manager/src/vm_payload_service.rs b/microdroid_manager/src/vm_payload_service.rs
index 5b5fb9e..0661314 100644
--- a/microdroid_manager/src/vm_payload_service.rs
+++ b/microdroid_manager/src/vm_payload_service.rs
@@ -15,16 +15,38 @@
 //! Implementation of the AIDL interface `IVmPayloadService`.
 
 use android_system_virtualization_payload::aidl::android::system::virtualization::payload::IVmPayloadService::{
-    BnVmPayloadService, IVmPayloadService, VM_PAYLOAD_SERVICE_SOCKET_NAME};
+    BnVmPayloadService, IVmPayloadService, VM_PAYLOAD_SERVICE_SOCKET_NAME, AttestationResult::AttestationResult,
+    STATUS_FAILED_TO_PREPARE_CSR_AND_KEY
+};
 use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice::IVirtualMachineService::IVirtualMachineService;
 use anyhow::{anyhow, Context, Result};
 use avflog::LogResult;
-use binder::{Interface, BinderFeatures, ExceptionCode, Strong, IntoBinderResult};
-use diced_open_dice::DiceArtifacts;
+use binder::{Interface, BinderFeatures, ExceptionCode, Strong, IntoBinderResult, Status};
+use diced_open_dice::{DiceArtifacts, derive_cdi_leaf_priv, PrivateKey, sign};
 use log::info;
 use rpcbinder::RpcServer;
+
+use crate::vm_secret::VmSecret;
+use coset::{
+    iana, CborSerializable, CoseKey, CoseKeyBuilder, CoseSign, CoseSignBuilder, CoseSignature,
+    CoseSignatureBuilder, HeaderBuilder,
+};
+use openssl::{
+    bn::{BigNum, BigNumContext},
+    ec::{EcGroup, EcKey, EcKeyRef},
+    ecdsa::EcdsaSig,
+    nid::Nid,
+    pkey::Private,
+    sha::sha256,
+};
+use service_vm_comm::{Csr, CsrPayload};
 use std::os::unix::io::OwnedFd;
-use crate::vm_secret::{VmSecret};
+use zeroize::Zeroizing;
+
+const ATTESTATION_KEY_NID: Nid = Nid::X9_62_PRIME256V1; // NIST P-256 curve
+const ATTESTATION_KEY_ALGO: iana::Algorithm = iana::Algorithm::ES256;
+const ATTESTATION_KEY_CURVE: iana::EllipticCurve = iana::EllipticCurve::P_256;
+const ATTESTATION_KEY_AFFINE_COORDINATE_SIZE: i32 = 32;
 
 /// Implementation of `IVmPayloadService`.
 struct VmPayloadService {
@@ -66,12 +88,111 @@
         Ok(self.secret.dice().cdi_attest().to_vec())
     }
 
-    fn requestAttestation(&self, challenge: &[u8]) -> binder::Result<Vec<u8>> {
+    fn requestAttestation(&self, challenge: &[u8]) -> binder::Result<AttestationResult> {
         self.check_restricted_apis_allowed()?;
-        self.virtual_machine_service.requestAttestation(challenge)
+        let (private_key, csr) = generate_attestation_key_and_csr(challenge, self.secret.dice())
+            .map_err(|e| {
+                Status::new_service_specific_error_str(
+                    STATUS_FAILED_TO_PREPARE_CSR_AND_KEY,
+                    Some(format!("Failed to prepare the CSR and key pair: {e:?}")),
+                )
+            })
+            .with_log()?;
+        let cert_chain = self.virtual_machine_service.requestAttestation(&csr)?;
+        Ok(AttestationResult {
+            privateKey: private_key.as_slice().to_vec(),
+            certificateChain: cert_chain,
+        })
     }
 }
 
+fn generate_attestation_key_and_csr(
+    challenge: &[u8],
+    dice_artifacts: &dyn DiceArtifacts,
+) -> Result<(Zeroizing<Vec<u8>>, Vec<u8>)> {
+    let group = EcGroup::from_curve_name(ATTESTATION_KEY_NID)?;
+    let attestation_key = EcKey::generate(&group)?;
+    let csr = build_csr(challenge, attestation_key.as_ref(), dice_artifacts)?;
+
+    let csr = csr.into_cbor_vec().context("Failed to serialize CSR")?;
+    let private_key = attestation_key.private_key_to_der()?;
+    Ok((Zeroizing::new(private_key), csr))
+}
+
+fn build_csr(
+    challenge: &[u8],
+    attestation_key: &EcKeyRef<Private>,
+    dice_artifacts: &dyn DiceArtifacts,
+) -> Result<Csr> {
+    // Builds CSR Payload to be signed.
+    let public_key =
+        to_cose_public_key(attestation_key)?.to_vec().context("Failed to serialize public key")?;
+    let csr_payload = CsrPayload { public_key, challenge: challenge.to_vec() };
+    let csr_payload = csr_payload.into_cbor_vec()?;
+
+    // Builds signed CSR Payload.
+    let cdi_leaf_priv = derive_cdi_leaf_priv(dice_artifacts)?;
+    let signed_csr_payload = build_signed_data(csr_payload, &cdi_leaf_priv, attestation_key)?
+        .to_vec()
+        .context("Failed to serialize signed CSR payload")?;
+
+    // Builds CSR.
+    let dice_cert_chain = dice_artifacts.bcc().ok_or(anyhow!("bcc is none"))?.to_vec();
+    Ok(Csr { dice_cert_chain, signed_csr_payload })
+}
+
+fn build_signed_data(
+    payload: Vec<u8>,
+    cdi_leaf_priv: &PrivateKey,
+    attestation_key: &EcKeyRef<Private>,
+) -> Result<CoseSign> {
+    let cdi_leaf_sig_headers = build_signature_headers(iana::Algorithm::EdDSA);
+    let attestation_key_sig_headers = build_signature_headers(ATTESTATION_KEY_ALGO);
+    let aad = &[];
+    let signed_data = CoseSignBuilder::new()
+        .payload(payload)
+        .try_add_created_signature(cdi_leaf_sig_headers, aad, |message| {
+            sign(message, cdi_leaf_priv.as_array()).map(|v| v.to_vec())
+        })?
+        .try_add_created_signature(attestation_key_sig_headers, aad, |message| {
+            ecdsa_sign(message, attestation_key)
+        })?
+        .build();
+    Ok(signed_data)
+}
+
+/// Builds a signature with headers filled with the provided algorithm.
+/// The signature data will be filled later when building the signed data.
+fn build_signature_headers(alg: iana::Algorithm) -> CoseSignature {
+    let protected = HeaderBuilder::new().algorithm(alg).build();
+    CoseSignatureBuilder::new().protected(protected).build()
+}
+
+fn ecdsa_sign(message: &[u8], key: &EcKeyRef<Private>) -> Result<Vec<u8>> {
+    let digest = sha256(message);
+    // Passes the digest to `ECDSA_do_sign` as recommended in the spec:
+    // https://commondatastorage.googleapis.com/chromium-boringssl-docs/ecdsa.h.html#ECDSA_do_sign
+    let sig = EcdsaSig::sign::<Private>(&digest, key)?;
+    Ok(sig.to_der()?)
+}
+
+fn get_affine_coordinates(key: &EcKeyRef<Private>) -> Result<(Vec<u8>, Vec<u8>)> {
+    let mut ctx = BigNumContext::new()?;
+    let mut x = BigNum::new()?;
+    let mut y = BigNum::new()?;
+    key.public_key().affine_coordinates_gfp(key.group(), &mut x, &mut y, &mut ctx)?;
+    let x = x.to_vec_padded(ATTESTATION_KEY_AFFINE_COORDINATE_SIZE)?;
+    let y = y.to_vec_padded(ATTESTATION_KEY_AFFINE_COORDINATE_SIZE)?;
+    Ok((x, y))
+}
+
+fn to_cose_public_key(key: &EcKeyRef<Private>) -> Result<CoseKey> {
+    let (x, y) = get_affine_coordinates(key)?;
+    Ok(CoseKeyBuilder::new_ec2_pub_key(ATTESTATION_KEY_CURVE, x, y)
+        .algorithm(ATTESTATION_KEY_ALGO)
+        .build())
+}
+
 impl Interface for VmPayloadService {}
 
 impl VmPayloadService {
@@ -116,3 +237,106 @@
     });
     Ok(())
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use anyhow::bail;
+    use ciborium::Value;
+    use coset::{iana::EnumI64, Label};
+    use hwtrust::{dice, session::Session};
+    use openssl::pkey::Public;
+
+    /// The following data is generated randomly with urandom.
+    const CHALLENGE: [u8; 16] = [
+        0xb3, 0x66, 0xfa, 0x72, 0x92, 0x32, 0x2c, 0xd4, 0x99, 0xcb, 0x00, 0x1f, 0x0e, 0xe0, 0xc7,
+        0x41,
+    ];
+
+    #[test]
+    fn csr_and_private_key_have_correct_format() -> Result<()> {
+        let dice_artifacts = diced_sample_inputs::make_sample_bcc_and_cdis()?;
+
+        let (private_key, csr) = generate_attestation_key_and_csr(&CHALLENGE, &dice_artifacts)?;
+        let ec_private_key = EcKey::private_key_from_der(&private_key)?;
+        let csr = Csr::from_cbor_slice(&csr).unwrap();
+        let cose_sign = CoseSign::from_slice(&csr.signed_csr_payload).unwrap();
+        let aad = &[];
+
+        // Checks CSR payload.
+        let csr_payload =
+            cose_sign.payload.as_ref().and_then(|v| CsrPayload::from_cbor_slice(v).ok()).unwrap();
+        let public_key = to_cose_public_key(&ec_private_key)?.to_vec().unwrap();
+        let expected_csr_payload = CsrPayload { challenge: CHALLENGE.to_vec(), public_key };
+        assert_eq!(expected_csr_payload, csr_payload);
+
+        // Checks the first signature is signed with CDI_Leaf_Priv.
+        let session = Session::default();
+        let chain = dice::Chain::from_cbor(&session, &csr.dice_cert_chain)?;
+        let public_key = chain.leaf().subject_public_key();
+        cose_sign
+            .verify_signature(0, aad, |signature, message| public_key.verify(signature, message))?;
+
+        // Checks the second signature is signed with attestation key.
+        let attestation_public_key = CoseKey::from_slice(&csr_payload.public_key).unwrap();
+        let ec_public_key = to_ec_public_key(&attestation_public_key)?;
+        cose_sign.verify_signature(1, aad, |signature, message| {
+            ecdsa_verify(signature, message, &ec_public_key)
+        })?;
+
+        // Verifies that private key and the public key form a valid key pair.
+        let message = b"test message";
+        let signature = ecdsa_sign(message, &ec_private_key)?;
+        ecdsa_verify(&signature, message, &ec_public_key)?;
+
+        Ok(())
+    }
+
+    fn ecdsa_verify(
+        signature: &[u8],
+        message: &[u8],
+        ec_public_key: &EcKeyRef<Public>,
+    ) -> Result<()> {
+        let sig = EcdsaSig::from_der(signature)?;
+        let digest = sha256(message);
+        if sig.verify(&digest, ec_public_key)? {
+            Ok(())
+        } else {
+            bail!("Signature does not match")
+        }
+    }
+
+    fn to_ec_public_key(cose_key: &CoseKey) -> Result<EcKey<Public>> {
+        check_ec_key_params(cose_key)?;
+        let group = EcGroup::from_curve_name(ATTESTATION_KEY_NID)?;
+        let x = get_label_value_as_bignum(cose_key, Label::Int(iana::Ec2KeyParameter::X.to_i64()))?;
+        let y = get_label_value_as_bignum(cose_key, Label::Int(iana::Ec2KeyParameter::Y.to_i64()))?;
+        let key = EcKey::from_public_key_affine_coordinates(&group, &x, &y)?;
+        key.check_key()?;
+        Ok(key)
+    }
+
+    fn check_ec_key_params(cose_key: &CoseKey) -> Result<()> {
+        assert_eq!(coset::KeyType::Assigned(iana::KeyType::EC2), cose_key.kty);
+        assert_eq!(Some(coset::Algorithm::Assigned(ATTESTATION_KEY_ALGO)), cose_key.alg);
+        let crv = get_label_value(cose_key, Label::Int(iana::Ec2KeyParameter::Crv.to_i64()))?;
+        assert_eq!(&Value::from(ATTESTATION_KEY_CURVE.to_i64()), crv);
+        Ok(())
+    }
+
+    fn get_label_value_as_bignum(key: &CoseKey, label: Label) -> Result<BigNum> {
+        get_label_value(key, label)?
+            .as_bytes()
+            .map(|v| BigNum::from_slice(&v[..]).unwrap())
+            .ok_or_else(|| anyhow!("Value not a bstr."))
+    }
+
+    fn get_label_value(key: &CoseKey, label: Label) -> Result<&Value> {
+        Ok(&key
+            .params
+            .iter()
+            .find(|(k, _)| k == &label)
+            .ok_or_else(|| anyhow!("Label {:?} not found", label))?
+            .1)
+    }
+}
diff --git a/pvmfw/Android.bp b/pvmfw/Android.bp
index 8c21030..b7b5900 100644
--- a/pvmfw/Android.bp
+++ b/pvmfw/Android.bp
@@ -59,6 +59,55 @@
     ],
 }
 
+genrule {
+    name: "test_pvmfw_devices_vm_dtbo",
+    defaults: ["dts_to_dtb"],
+    srcs: ["testdata/test_pvmfw_devices_vm_dtbo.dts"],
+    out: ["test_pvmfw_devices_vm_dtbo.dtbo"],
+}
+
+genrule {
+    name: "test_pvmfw_devices_vm_dtbo_without_symbols",
+    defaults: ["dts_to_dtb"],
+    srcs: ["testdata/test_pvmfw_devices_vm_dtbo_without_symbols.dts"],
+    out: ["test_pvmfw_devices_vm_dtbo_without_symbols.dtbo"],
+}
+
+genrule {
+    name: "test_pvmfw_devices_with_rng",
+    defaults: ["dts_to_dtb"],
+    srcs: ["testdata/test_pvmfw_devices_with_rng.dts"],
+    out: ["test_pvmfw_devices_with_rng.dtb"],
+}
+
+rust_test {
+    name: "libpvmfw.device_assignment.test",
+    srcs: ["src/device_assignment.rs"],
+    defaults: ["avf_build_flags_rust"],
+    test_suites: ["general-tests"],
+    test_options: {
+        unit_test: true,
+    },
+    prefer_rlib: true,
+    rustlibs: [
+        "liblibfdt",
+        "liblog_rust",
+        "libpvmfw_fdt_template",
+    ],
+    data: [
+        ":test_pvmfw_devices_vm_dtbo",
+        ":test_pvmfw_devices_vm_dtbo_without_symbols",
+        ":test_pvmfw_devices_with_rng",
+    ],
+    // To use libpvmfw_fdt_template for testing
+    enabled: false,
+    target: {
+        android_arm64: {
+            enabled: true,
+        },
+    },
+}
+
 cc_binary {
     name: "pvmfw",
     defaults: ["vmbase_elf_defaults"],
@@ -175,7 +224,7 @@
     srcs: [":pvmfw_platform.dts.preprocessed"],
     out: ["lib.rs"],
     tools: ["dtc"],
-    cmd: "$(location dtc) -I dts -O dtb -o $(genDir)/compiled.dtbo $(in) && " +
+    cmd: "$(location dtc) -@ -I dts -O dtb -o $(genDir)/compiled.dtbo $(in) && " +
         "(" +
         "    echo '#![no_std]';" +
         "    echo '#![allow(missing_docs)]';" +
diff --git a/pvmfw/TEST_MAPPING b/pvmfw/TEST_MAPPING
index d77e651..f21318e 100644
--- a/pvmfw/TEST_MAPPING
+++ b/pvmfw/TEST_MAPPING
@@ -7,6 +7,9 @@
     },
     {
       "name" : "libpvmfw.bootargs.test"
+    },
+    {
+      "name" : "libpvmfw.device_assignment.test"
     }
   ]
 }
diff --git a/pvmfw/platform.dts b/pvmfw/platform.dts
index cb8e30d..4a269c3 100644
--- a/pvmfw/platform.dts
+++ b/pvmfw/platform.dts
@@ -261,4 +261,64 @@
 		clock-frequency = <10>;
 		timeout-sec = <8>;
 	};
+
+	pviommu_0: pviommu0 {
+		compatible = "pkvm,pviommu";
+		id = <PLACEHOLDER>;
+		#iommu-cells = <0>;
+	};
+
+	pviommu_1: pviommu1 {
+		compatible = "pkvm,pviommu";
+		id = <PLACEHOLDER>;
+		#iommu-cells = <0>;
+	};
+
+	pviommu_2: pviommu2 {
+		compatible = "pkvm,pviommu";
+		id = <PLACEHOLDER>;
+		#iommu-cells = <0>;
+	};
+
+	pviommu_3: pviommu3 {
+		compatible = "pkvm,pviommu";
+		id = <PLACEHOLDER>;
+		#iommu-cells = <0>;
+	};
+
+	pviommu_4: pviommu4 {
+		compatible = "pkvm,pviommu";
+		id = <PLACEHOLDER>;
+		#iommu-cells = <0>;
+	};
+
+	pviommu_5: pviommu5 {
+		compatible = "pkvm,pviommu";
+		id = <PLACEHOLDER>;
+		#iommu-cells = <0>;
+	};
+
+	pviommu_6: pviommu6 {
+		compatible = "pkvm,pviommu";
+		id = <PLACEHOLDER>;
+		#iommu-cells = <0>;
+	};
+
+	pviommu_7: pviommu7 {
+		compatible = "pkvm,pviommu";
+		id = <PLACEHOLDER>;
+		#iommu-cells = <0>;
+	};
+
+	pviommu_8: pviommu8 {
+		compatible = "pkvm,pviommu";
+		id = <PLACEHOLDER>;
+		#iommu-cells = <0>;
+	};
+
+	pviommu_9: pviommu9 {
+		compatible = "pkvm,pviommu";
+		id = <PLACEHOLDER>;
+		#iommu-cells = <0>;
+	};
 };
diff --git a/pvmfw/src/bootargs.rs b/pvmfw/src/bootargs.rs
index e4b1b65..a089a67 100644
--- a/pvmfw/src/bootargs.rs
+++ b/pvmfw/src/bootargs.rs
@@ -94,7 +94,7 @@
         // after.
         let name_end = arg.find(|c: char| c.is_whitespace() || c == '=').unwrap_or(arg.len());
         let (arg, equal_sign) = match arg.chars().nth(name_end) {
-            Some(c) if c == '=' => {
+            Some('=') => {
                 let value_end = name_end + Self::find_value_end(&arg[name_end..]);
                 (&arg[..value_end], Some(name_end))
             }
@@ -109,9 +109,16 @@
 mod tests {
     use super::*;
 
+    // TODO(b/308694211): Use cstr!() from vmbase
     macro_rules! cstr {
         ($str:literal) => {{
-            core::ffi::CStr::from_bytes_with_nul(concat!($str, "\0").as_bytes()).unwrap()
+            const S: &str = concat!($str, "\0");
+            const C: &::core::ffi::CStr = match ::core::ffi::CStr::from_bytes_with_nul(S.as_bytes())
+            {
+                Ok(v) => v,
+                Err(_) => panic!("string contains interior NUL"),
+            };
+            C
         }};
     }
 
diff --git a/pvmfw/src/config.rs b/pvmfw/src/config.rs
index 78b6323..7023b95 100644
--- a/pvmfw/src/config.rs
+++ b/pvmfw/src/config.rs
@@ -260,7 +260,7 @@
     }
 
     /// Get slice containing the platform BCC.
-    pub fn get_entries(&mut self) -> (&mut [u8], Option<&mut [u8]>) {
+    pub fn get_entries(&mut self) -> (&mut [u8], Option<&mut [u8]>, Option<&mut [u8]>) {
         // This assumes that the blobs are in-order w.r.t. the entries.
         let bcc_range = self.get_entry_range(Entry::Bcc);
         let dp_range = self.get_entry_range(Entry::DebugPolicy);
@@ -277,6 +277,7 @@
             (
                 Self::from_raw_range_mut(ptr, bcc_range.unwrap()),
                 dp_range.map(|dp_range| Self::from_raw_range_mut(ptr, dp_range)),
+                vm_dtbo_range.map(|vm_dtbo_range| Self::from_raw_range_mut(ptr, vm_dtbo_range)),
             )
         }
     }
diff --git a/pvmfw/src/device_assignment.rs b/pvmfw/src/device_assignment.rs
new file mode 100644
index 0000000..a92b418
--- /dev/null
+++ b/pvmfw/src/device_assignment.rs
@@ -0,0 +1,428 @@
+// Copyright 2023, The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Validate device assignment written in crosvm DT with VM DTBO, and apply it
+//! to platform DT.
+//! Declared in separated libs for adding unit tests, which requires libstd.
+
+#[cfg(test)]
+extern crate alloc;
+
+use alloc::ffi::CString;
+use alloc::fmt;
+use alloc::vec;
+use alloc::vec::Vec;
+use core::ffi::CStr;
+use core::iter::Iterator;
+use core::mem;
+use libfdt::{Fdt, FdtError, FdtNode};
+
+// TODO(b/308694211): Use cstr! from vmbase instead.
+macro_rules! cstr {
+    ($str:literal) => {{
+        const S: &str = concat!($str, "\0");
+        const C: &::core::ffi::CStr = match ::core::ffi::CStr::from_bytes_with_nul(S.as_bytes()) {
+            Ok(v) => v,
+            Err(_) => panic!("string contains interior NUL"),
+        };
+        C
+    }};
+}
+
+// TODO(b/277993056): Keep constants derived from platform.dts in one place.
+const CELLS_PER_INTERRUPT: usize = 3; // from /intc node in platform.dts
+
+/// Errors in device assignment.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum DeviceAssignmentError {
+    // Invalid VM DTBO
+    InvalidDtbo,
+    /// Invalid __symbols__
+    InvalidSymbols,
+    /// Invalid <interrupts>
+    InvalidInterrupts,
+    /// Unsupported overlay target syntax. Only supports <target-path> with full path.
+    UnsupportedOverlayTarget,
+    /// Unexpected error from libfdt
+    UnexpectedFdtError(FdtError),
+}
+
+impl From<FdtError> for DeviceAssignmentError {
+    fn from(e: FdtError) -> Self {
+        DeviceAssignmentError::UnexpectedFdtError(e)
+    }
+}
+
+impl fmt::Display for DeviceAssignmentError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Self::InvalidDtbo => write!(f, "Invalid DTBO"),
+            Self::InvalidSymbols => write!(
+                f,
+                "Invalid property in /__symbols__. Must point to valid assignable device node."
+            ),
+            Self::InvalidInterrupts => write!(f, "Invalid <interrupts>"),
+            Self::UnsupportedOverlayTarget => {
+                write!(f, "Unsupported overlay target. Only supports 'target-path = \"/\"'")
+            }
+            Self::UnexpectedFdtError(e) => write!(f, "Unexpected Error from libfdt: {e}"),
+        }
+    }
+}
+
+pub type Result<T> = core::result::Result<T, DeviceAssignmentError>;
+
+/// Represents VM DTBO
+#[repr(transparent)]
+pub struct VmDtbo(Fdt);
+
+impl VmDtbo {
+    /// Wraps a mutable slice containing a VM DTBO.
+    ///
+    /// Fails if the VM DTBO does not pass validation.
+    pub fn from_mut_slice(dtbo: &mut [u8]) -> Result<&mut Self> {
+        // This validates DTBO
+        let fdt = Fdt::from_mut_slice(dtbo)?;
+        // SAFETY: VmDtbo is a transparent wrapper around Fdt, so representation is the same.
+        Ok(unsafe { mem::transmute::<&mut Fdt, &mut Self>(fdt) })
+    }
+
+    // Locates device node path as if the given dtbo node path is assigned and VM DTBO is overlaid.
+    // For given dtbo node path, this concatenates <target-path> of the enclosing fragment and
+    // relative path from __overlay__ node.
+    //
+    // Here's an example with sample VM DTBO:
+    //    / {
+    //       fragment@rng {
+    //         target-path = "/";  // Always 'target-path = "/"'. Disallows <target> or other path.
+    //         __overlay__ {
+    //           rng { ... };      // Actual device node is here. If overlaid, path would be "/rng"
+    //         };
+    //       };
+    //       __symbols__ {         // List of assignable devices
+    //         // Each property describes an assigned device device information.
+    //         // property name is the device label, and property value is the path in the VM DTBO.
+    //         rng = "/fragment@rng/__overlay__/rng";
+    //       };
+    //    };
+    //
+    // Then locate_overlay_target_path(cstr!("/fragment@rng/__overlay__/rng")) is Ok("/rng")
+    //
+    // Contrary to fdt_overlay_target_offset(), this API enforces overlay target property
+    // 'target-path = "/"', so the overlay doesn't modify and/or append platform DT's existing
+    // node and/or properties. The enforcement is for compatibility reason.
+    fn locate_overlay_target_path(&self, dtbo_node_path: &CStr) -> Result<CString> {
+        let dtbo_node_path_bytes = dtbo_node_path.to_bytes();
+        if dtbo_node_path_bytes.first() != Some(&b'/') {
+            return Err(DeviceAssignmentError::UnsupportedOverlayTarget);
+        }
+
+        let node = self.0.node(dtbo_node_path)?.ok_or(DeviceAssignmentError::InvalidSymbols)?;
+
+        let fragment_node = node.supernode_at_depth(1)?;
+        let target_path = fragment_node
+            .getprop_str(cstr!("target-path"))?
+            .ok_or(DeviceAssignmentError::InvalidDtbo)?;
+        if target_path != cstr!("/") {
+            return Err(DeviceAssignmentError::UnsupportedOverlayTarget);
+        }
+
+        let mut components = dtbo_node_path_bytes
+            .split(|char| *char == b'/')
+            .filter(|&component| !component.is_empty())
+            .skip(1);
+        let overlay_node_name = components.next();
+        if overlay_node_name != Some(b"__overlay__") {
+            return Err(DeviceAssignmentError::InvalidDtbo);
+        }
+        let mut overlaid_path = Vec::with_capacity(dtbo_node_path_bytes.len());
+        for component in components {
+            overlaid_path.push(b'/');
+            overlaid_path.extend_from_slice(component);
+        }
+        overlaid_path.push(b'\0');
+
+        Ok(CString::from_vec_with_nul(overlaid_path).unwrap())
+    }
+}
+
+impl AsRef<Fdt> for VmDtbo {
+    fn as_ref(&self) -> &Fdt {
+        &self.0
+    }
+}
+
+impl AsMut<Fdt> for VmDtbo {
+    fn as_mut(&mut self) -> &mut Fdt {
+        &mut self.0
+    }
+}
+
+/// Assigned device information parsed from crosvm DT.
+/// Keeps everything in the owned data because underlying FDT will be reused for platform DT.
+#[derive(Debug, Eq, PartialEq)]
+struct AssignedDeviceInfo {
+    // Node path of assigned device (e.g. "/rng")
+    node_path: CString,
+    // DTBO node path of the assigned device (e.g. "/fragment@rng/__overlay__/rng")
+    dtbo_node_path: CString,
+    // <reg> property from the crosvm DT
+    reg: Vec<u8>,
+    // <interrupts> property from the crosvm DT
+    interrupts: Vec<u8>,
+}
+
+impl AssignedDeviceInfo {
+    fn parse_interrupts(node: &FdtNode) -> Result<Vec<u8>> {
+        // Validation: Validate if interrupts cell numbers are multiple of #interrupt-cells.
+        // We can't know how many interrupts would exist.
+        let interrupts_cells = node
+            .getprop_cells(cstr!("interrupts"))?
+            .ok_or(DeviceAssignmentError::InvalidInterrupts)?
+            .count();
+        if interrupts_cells % CELLS_PER_INTERRUPT != 0 {
+            return Err(DeviceAssignmentError::InvalidInterrupts);
+        }
+
+        // Once validated, keep the raw bytes so patch can be done with setprop()
+        Ok(node.getprop(cstr!("interrupts")).unwrap().unwrap().into())
+    }
+
+    // TODO(b/277993056): Read and validate iommu
+    fn parse(fdt: &Fdt, vm_dtbo: &VmDtbo, dtbo_node_path: &CStr) -> Result<Option<Self>> {
+        let node_path = vm_dtbo.locate_overlay_target_path(dtbo_node_path)?;
+
+        let Some(node) = fdt.node(&node_path)? else { return Ok(None) };
+
+        // TODO(b/277993056): Validate reg with HVC, and keep reg with FdtNode::reg()
+        let reg = node.getprop(cstr!("reg")).unwrap().unwrap();
+
+        let interrupts = Self::parse_interrupts(&node)?;
+
+        Ok(Some(Self {
+            node_path,
+            dtbo_node_path: dtbo_node_path.into(),
+            reg: reg.to_vec(),
+            interrupts: interrupts.to_vec(),
+        }))
+    }
+
+    fn patch(&self, fdt: &mut Fdt) -> Result<()> {
+        let mut dst = fdt.node_mut(&self.node_path)?.unwrap();
+        dst.setprop(cstr!("reg"), &self.reg)?;
+        dst.setprop(cstr!("interrupts"), &self.interrupts)?;
+        // TODO(b/277993056): Read and patch iommu
+        Ok(())
+    }
+}
+
+#[derive(Debug, Default, Eq, PartialEq)]
+pub struct DeviceAssignmentInfo {
+    assigned_devices: Vec<AssignedDeviceInfo>,
+    filtered_dtbo_paths: Vec<CString>,
+}
+
+impl DeviceAssignmentInfo {
+    /// Parses fdt and vm_dtbo, and creates new DeviceAssignmentInfo
+    // TODO(b/277993056): Parse __local_fixups__
+    // TODO(b/277993056): Parse __fixups__
+    pub fn parse(fdt: &Fdt, vm_dtbo: &VmDtbo) -> Result<Option<Self>> {
+        let Some(symbols_node) = vm_dtbo.as_ref().symbols()? else {
+            // /__symbols__ should contain all assignable devices.
+            // If empty, then nothing can be assigned.
+            return Ok(None);
+        };
+
+        let mut assigned_devices = vec![];
+        let mut filtered_dtbo_paths = vec![];
+        for symbol_prop in symbols_node.properties()? {
+            let symbol_prop_value = symbol_prop.value()?;
+            let dtbo_node_path = CStr::from_bytes_with_nul(symbol_prop_value)
+                .or(Err(DeviceAssignmentError::InvalidSymbols))?;
+            let assigned_device = AssignedDeviceInfo::parse(fdt, vm_dtbo, dtbo_node_path)?;
+            if let Some(assigned_device) = assigned_device {
+                assigned_devices.push(assigned_device);
+            } else {
+                filtered_dtbo_paths.push(dtbo_node_path.into());
+            }
+        }
+        filtered_dtbo_paths.push(CString::new("/__symbols__").unwrap());
+
+        if assigned_devices.is_empty() {
+            return Ok(None);
+        }
+        Ok(Some(Self { assigned_devices, filtered_dtbo_paths }))
+    }
+
+    /// Filters VM DTBO to only contain necessary information for booting pVM
+    /// In detail, this will remove followings by setting nop node / nop property.
+    ///   - Removes unassigned devices
+    ///   - Removes /__symbols__ node
+    // TODO(b/277993056): remove unused dependencies in VM DTBO.
+    // TODO(b/277993056): remove supernodes' properties.
+    // TODO(b/277993056): remove unused alises.
+    pub fn filter(&self, vm_dtbo: &mut VmDtbo) -> Result<()> {
+        let vm_dtbo = vm_dtbo.as_mut();
+
+        // Filters unused node in assigned devices
+        for filtered_dtbo_path in &self.filtered_dtbo_paths {
+            let node = vm_dtbo.node_mut(filtered_dtbo_path).unwrap().unwrap();
+            node.nop()?;
+        }
+
+        // Filters pvmfw-specific properties in assigned device node.
+        const FILTERED_VM_DTBO_PROP: [&CStr; 3] = [
+            cstr!("android,pvmfw,phy-reg"),
+            cstr!("android,pvmfw,phy-iommu"),
+            cstr!("android,pvmfw,phy-sid"),
+        ];
+        for assigned_device in &self.assigned_devices {
+            let mut node = vm_dtbo.node_mut(&assigned_device.dtbo_node_path).unwrap().unwrap();
+            for prop in FILTERED_VM_DTBO_PROP {
+                node.nop_property(prop)?;
+            }
+        }
+        Ok(())
+    }
+
+    pub fn patch(&self, fdt: &mut Fdt) -> Result<()> {
+        for device in &self.assigned_devices {
+            device.patch(fdt)?
+        }
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::fs;
+
+    const VM_DTBO_FILE_PATH: &str = "test_pvmfw_devices_vm_dtbo.dtbo";
+    const VM_DTBO_WITHOUT_SYMBOLS_FILE_PATH: &str =
+        "test_pvmfw_devices_vm_dtbo_without_symbols.dtbo";
+    const FDT_FILE_PATH: &str = "test_pvmfw_devices_with_rng.dtb";
+
+    fn into_fdt_prop(native_bytes: Vec<u32>) -> Vec<u8> {
+        let mut v = Vec::with_capacity(native_bytes.len() * 4);
+        for byte in native_bytes {
+            v.extend_from_slice(&byte.to_be_bytes());
+        }
+        v
+    }
+
+    #[test]
+    fn device_info_new_without_symbols() {
+        let mut fdt_data = fs::read(FDT_FILE_PATH).unwrap();
+        let mut vm_dtbo_data = fs::read(VM_DTBO_WITHOUT_SYMBOLS_FILE_PATH).unwrap();
+        let fdt = Fdt::from_mut_slice(&mut fdt_data).unwrap();
+        let vm_dtbo = VmDtbo::from_mut_slice(&mut vm_dtbo_data).unwrap();
+
+        let device_info = DeviceAssignmentInfo::parse(fdt, vm_dtbo).unwrap();
+        assert_eq!(device_info, None);
+    }
+
+    #[test]
+    fn device_info_assigned_info() {
+        let mut fdt_data = fs::read(FDT_FILE_PATH).unwrap();
+        let mut vm_dtbo_data = fs::read(VM_DTBO_FILE_PATH).unwrap();
+        let fdt = Fdt::from_mut_slice(&mut fdt_data).unwrap();
+        let vm_dtbo = VmDtbo::from_mut_slice(&mut vm_dtbo_data).unwrap();
+
+        let device_info = DeviceAssignmentInfo::parse(fdt, vm_dtbo).unwrap().unwrap();
+
+        let expected = [AssignedDeviceInfo {
+            node_path: CString::new("/rng").unwrap(),
+            dtbo_node_path: cstr!("/fragment@rng/__overlay__/rng").into(),
+            reg: into_fdt_prop(vec![0x0, 0x9, 0x0, 0xFF]),
+            interrupts: into_fdt_prop(vec![0x0, 0xF, 0x4]),
+        }];
+
+        assert_eq!(device_info.assigned_devices, expected);
+    }
+
+    #[test]
+    fn device_info_new_without_assigned_devices() {
+        let mut fdt_data: Vec<u8> = pvmfw_fdt_template::RAW.into();
+        let mut vm_dtbo_data = fs::read(VM_DTBO_FILE_PATH).unwrap();
+        let fdt = Fdt::from_mut_slice(fdt_data.as_mut_slice()).unwrap();
+        let vm_dtbo = VmDtbo::from_mut_slice(&mut vm_dtbo_data).unwrap();
+
+        let device_info = DeviceAssignmentInfo::parse(fdt, vm_dtbo).unwrap();
+        assert_eq!(device_info, None);
+    }
+
+    #[test]
+    fn device_info_filter() {
+        let mut fdt_data = fs::read(FDT_FILE_PATH).unwrap();
+        let mut vm_dtbo_data = fs::read(VM_DTBO_FILE_PATH).unwrap();
+        let fdt = Fdt::from_mut_slice(&mut fdt_data).unwrap();
+        let vm_dtbo = VmDtbo::from_mut_slice(&mut vm_dtbo_data).unwrap();
+
+        let device_info = DeviceAssignmentInfo::parse(fdt, vm_dtbo).unwrap().unwrap();
+        device_info.filter(vm_dtbo).unwrap();
+
+        let vm_dtbo = vm_dtbo.as_mut();
+
+        let rng = vm_dtbo.node(cstr!("/fragment@rng/__overlay__/rng")).unwrap();
+        assert_ne!(rng, None);
+
+        let light = vm_dtbo.node(cstr!("/fragment@rng/__overlay__/light")).unwrap();
+        assert_eq!(light, None);
+
+        let symbols_node = vm_dtbo.symbols().unwrap();
+        assert_eq!(symbols_node, None);
+    }
+
+    #[test]
+    fn device_info_patch() {
+        let mut fdt_data = fs::read(FDT_FILE_PATH).unwrap();
+        let mut vm_dtbo_data = fs::read(VM_DTBO_FILE_PATH).unwrap();
+        let mut data = vec![0_u8; fdt_data.len() + vm_dtbo_data.len()];
+        let fdt = Fdt::from_mut_slice(&mut fdt_data).unwrap();
+        let vm_dtbo = VmDtbo::from_mut_slice(&mut vm_dtbo_data).unwrap();
+        let platform_dt = Fdt::create_empty_tree(data.as_mut_slice()).unwrap();
+
+        let device_info = DeviceAssignmentInfo::parse(fdt, vm_dtbo).unwrap().unwrap();
+        device_info.filter(vm_dtbo).unwrap();
+
+        // SAFETY: Damaged VM DTBO wouldn't be used after this unsafe block.
+        unsafe {
+            platform_dt.apply_overlay(vm_dtbo.as_mut()).unwrap();
+        }
+        device_info.patch(platform_dt).unwrap();
+
+        type FdtResult<T> = libfdt::Result<T>;
+        let expected: Vec<(FdtResult<&CStr>, FdtResult<Vec<u8>>)> = vec![
+            (Ok(cstr!("android,rng,ignore-gctrl-reset")), Ok(Vec::new())),
+            (Ok(cstr!("compatible")), Ok(Vec::from(*b"android,rng\0"))),
+            (Ok(cstr!("interrupts")), Ok(into_fdt_prop(vec![0x0, 0xF, 0x4]))),
+            (Ok(cstr!("reg")), Ok(into_fdt_prop(vec![0x0, 0x9, 0x0, 0xFF]))),
+        ];
+
+        let rng_node = platform_dt.node(cstr!("/rng")).unwrap().unwrap();
+        let mut properties: Vec<_> = rng_node
+            .properties()
+            .unwrap()
+            .map(|prop| (prop.name(), prop.value().map(|x| x.into())))
+            .collect();
+        properties.sort_by(|a, b| {
+            let lhs = a.0.unwrap_or_default();
+            let rhs = b.0.unwrap_or_default();
+            lhs.partial_cmp(rhs).unwrap()
+        });
+
+        assert_eq!(properties, expected);
+    }
+}
diff --git a/pvmfw/src/entry.rs b/pvmfw/src/entry.rs
index 9c929a9..ed73bc9 100644
--- a/pvmfw/src/entry.rs
+++ b/pvmfw/src/entry.rs
@@ -83,7 +83,12 @@
 }
 
 impl<'a> MemorySlices<'a> {
-    fn new(fdt: usize, kernel: usize, kernel_size: usize) -> Result<Self, RebootReason> {
+    fn new(
+        fdt: usize,
+        kernel: usize,
+        kernel_size: usize,
+        vm_dtbo: Option<&mut [u8]>,
+    ) -> Result<Self, RebootReason> {
         let fdt_size = NonZeroUsize::new(crosvm::FDT_MAX_SIZE).unwrap();
         // TODO - Only map the FDT as read-only, until we modify it right before jump_to_payload()
         // e.g. by generating a DTBO for a template DT in main() and, on return, re-map DT as RW,
@@ -95,12 +100,12 @@
 
         // 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 info = fdt::sanitize_device_tree(fdt, vm_dtbo)?;
         let fdt = libfdt::Fdt::from_mut_slice(fdt).map_err(|e| {
-            error!("Failed to spawn the FDT wrapper: {e}");
+            error!("Failed to load sanitized FDT: {e}");
             RebootReason::InvalidFdt
         })?;
-
-        let info = fdt::sanitize_device_tree(fdt)?;
         debug!("Fdt passed validation!");
 
         let memory_range = info.memory_range;
@@ -207,7 +212,7 @@
         RebootReason::InvalidConfig
     })?;
 
-    let (bcc_slice, debug_policy) = appended.get_entries();
+    let (bcc_slice, debug_policy, vm_dtbo) = appended.get_entries();
 
     // Up to this point, we were using the built-in static (from .rodata) page tables.
     MEMORY.lock().replace(MemoryTracker::new(
@@ -217,7 +222,7 @@
         Some(memory::appended_payload_range()),
     ));
 
-    let slices = MemorySlices::new(fdt, payload, payload_size)?;
+    let slices = MemorySlices::new(fdt, payload, payload_size, vm_dtbo)?;
 
     // This wrapper allows main() to be blissfully ignorant of platform details.
     let next_bcc = crate::main(slices.fdt, slices.kernel, slices.ramdisk, bcc_slice, debug_policy)?;
@@ -427,10 +432,10 @@
         }
     }
 
-    fn get_entries(&mut self) -> (&mut [u8], Option<&mut [u8]>) {
+    fn get_entries(&mut self) -> (&mut [u8], Option<&mut [u8]>, Option<&mut [u8]>) {
         match self {
             Self::Config(ref mut cfg) => cfg.get_entries(),
-            Self::LegacyBcc(ref mut bcc) => (bcc, None),
+            Self::LegacyBcc(ref mut bcc) => (bcc, None, None),
         }
     }
 }
diff --git a/pvmfw/src/fdt.rs b/pvmfw/src/fdt.rs
index 1f87dcc..7655614 100644
--- a/pvmfw/src/fdt.rs
+++ b/pvmfw/src/fdt.rs
@@ -15,6 +15,8 @@
 //! High-level FDT functions.
 
 use crate::bootargs::BootArgsIterator;
+use crate::device_assignment::DeviceAssignmentInfo;
+use crate::device_assignment::VmDtbo;
 use crate::helpers::GUEST_PAGE_SIZE;
 use crate::Box;
 use crate::RebootReason;
@@ -590,6 +592,7 @@
     pci_info: PciInfo,
     serial_info: SerialInfo,
     pub swiotlb_info: SwiotlbInfo,
+    device_assignment: Option<DeviceAssignmentInfo>,
 }
 
 impl DeviceTreeInfo {
@@ -600,20 +603,53 @@
     }
 }
 
-pub fn sanitize_device_tree(fdt: &mut Fdt) -> Result<DeviceTreeInfo, RebootReason> {
-    let info = parse_device_tree(fdt)?;
-    debug!("Device tree info: {:?}", info);
+pub fn sanitize_device_tree(
+    fdt: &mut [u8],
+    vm_dtbo: Option<&mut [u8]>,
+) -> Result<DeviceTreeInfo, RebootReason> {
+    let fdt = Fdt::from_mut_slice(fdt).map_err(|e| {
+        error!("Failed to load FDT: {e}");
+        RebootReason::InvalidFdt
+    })?;
+
+    let vm_dtbo = match vm_dtbo {
+        Some(vm_dtbo) => Some(VmDtbo::from_mut_slice(vm_dtbo).map_err(|e| {
+            error!("Failed to load VM DTBO: {e}");
+            RebootReason::InvalidFdt
+        })?),
+        None => None,
+    };
+
+    let info = parse_device_tree(fdt, vm_dtbo.as_deref())?;
 
     fdt.copy_from_slice(pvmfw_fdt_template::RAW).map_err(|e| {
         error!("Failed to instantiate FDT from the template DT: {e}");
         RebootReason::InvalidFdt
     })?;
 
+    if let Some(device_assignment_info) = &info.device_assignment {
+        let vm_dtbo = vm_dtbo.unwrap();
+        device_assignment_info.filter(vm_dtbo).map_err(|e| {
+            error!("Failed to filter VM DTBO: {e}");
+            RebootReason::InvalidFdt
+        })?;
+        // SAFETY: Damaged VM DTBO isn't used in this API after this unsafe block.
+        // VM DTBO can't be reused in any way as Fdt nor VmDtbo outside of this API because
+        // it can only be instantiated after validation.
+        unsafe {
+            fdt.apply_overlay(vm_dtbo.as_mut()).map_err(|e| {
+                error!("Failed to apply filtered VM DTBO: {e}");
+                RebootReason::InvalidFdt
+            })?;
+        }
+    }
+
     patch_device_tree(fdt, &info)?;
+
     Ok(info)
 }
 
-fn parse_device_tree(fdt: &libfdt::Fdt) -> Result<DeviceTreeInfo, RebootReason> {
+fn parse_device_tree(fdt: &Fdt, vm_dtbo: Option<&VmDtbo>) -> Result<DeviceTreeInfo, RebootReason> {
     let kernel_range = read_kernel_range_from(fdt).map_err(|e| {
         error!("Failed to read kernel range from DT: {e}");
         RebootReason::InvalidFdt
@@ -657,6 +693,14 @@
     })?;
     validate_swiotlb_info(&swiotlb_info, &memory_range)?;
 
+    let device_assignment = match vm_dtbo {
+        Some(vm_dtbo) => DeviceAssignmentInfo::parse(fdt, vm_dtbo).map_err(|e| {
+            error!("Failed to parse device assignment from DT and VM DTBO: {e}");
+            RebootReason::InvalidFdt
+        })?,
+        None => None,
+    };
+
     Ok(DeviceTreeInfo {
         kernel_range,
         initrd_range,
@@ -666,6 +710,7 @@
         pci_info,
         serial_info,
         swiotlb_info,
+        device_assignment,
     })
 }
 
@@ -715,6 +760,14 @@
         error!("Failed to patch timer info to DT: {e}");
         RebootReason::InvalidFdt
     })?;
+    if let Some(device_assignment) = &info.device_assignment {
+        // Note: We patch values after VM DTBO is overlaid because patch may require more space
+        // then VM DTBO's underlying slice is allocated.
+        device_assignment.patch(fdt).map_err(|e| {
+            error!("Failed to patch device assignment info to DT: {e}");
+            RebootReason::InvalidFdt
+        })?;
+    }
 
     fdt.pack().map_err(|e| {
         error!("Failed to pack DT after patching: {e}");
diff --git a/pvmfw/src/main.rs b/pvmfw/src/main.rs
index b8cbf1b..8aa5274 100644
--- a/pvmfw/src/main.rs
+++ b/pvmfw/src/main.rs
@@ -23,6 +23,7 @@
 mod bootargs;
 mod config;
 mod crypto;
+mod device_assignment;
 mod dice;
 mod entry;
 mod exceptions;
diff --git a/pvmfw/testdata/test_pvmfw_devices_vm_dtbo.dts b/pvmfw/testdata/test_pvmfw_devices_vm_dtbo.dts
new file mode 100644
index 0000000..e85b55b
--- /dev/null
+++ b/pvmfw/testdata/test_pvmfw_devices_vm_dtbo.dts
@@ -0,0 +1,32 @@
+/dts-v1/;
+/plugin/;
+
+/ {
+	fragment@rng {
+		target-path = "/";
+		__overlay__ {
+			rng {
+				compatible = "android,rng";
+				android,rng,ignore-gctrl-reset;
+				android,pvmfw,phy-reg = <0x0 0x12F00000 0x1000>;
+				android,pvmfw,phy-iommu = <0x0 0x12E40000>;
+				android,pvmfw,phy-sid = <3>;
+			};
+		};
+	};
+
+	fragment@sensor {
+		target-path = "/";
+		__overlay__ {
+			light {
+				compatible = "android,light";
+				version = <0x1 0x2>;
+			};
+		};
+	};
+
+	__symbols__ {
+		rng = "/fragment@rng/__overlay__/rng";
+		sensor = "/fragment@sensor/__overlay__/light";
+	};
+};
diff --git a/pvmfw/testdata/test_pvmfw_devices_vm_dtbo_without_symbols.dts b/pvmfw/testdata/test_pvmfw_devices_vm_dtbo_without_symbols.dts
new file mode 100644
index 0000000..08444ac
--- /dev/null
+++ b/pvmfw/testdata/test_pvmfw_devices_vm_dtbo_without_symbols.dts
@@ -0,0 +1,27 @@
+/dts-v1/;
+/plugin/;
+
+/ {
+	fragment@rng {
+		target-path = "/";
+		__overlay__ {
+			rng {
+				compatible = "android,rng";
+				android,rng,ignore-gctrl-reset;
+				android,pvmfw,phy-reg = <0x0 0x12F00000 0x1000>;
+				android,pvmfw,phy-iommu = <0x0 0x12E40000>;
+				android,pvmfw,phy-sid = <3>;
+			};
+		};
+	};
+
+	fragment@sensor {
+		target-path = "/";
+		__overlay__ {
+			light {
+				compatible = "android,light";
+				version = <0x1 0x2>;
+			};
+		};
+	};
+};
diff --git a/pvmfw/testdata/test_pvmfw_devices_with_rng.dts b/pvmfw/testdata/test_pvmfw_devices_with_rng.dts
new file mode 100644
index 0000000..f24fd65
--- /dev/null
+++ b/pvmfw/testdata/test_pvmfw_devices_with_rng.dts
@@ -0,0 +1,52 @@
+/dts-v1/;
+/plugin/;
+
+/ {
+	chosen {
+		stdout-path = "/uart@3f8";
+		linux,pci-probe-only = <1>;
+	};
+
+	memory {
+		device_type = "memory";
+		reg = <0x00 0x80000000 0xFFFFFFFF>;
+	};
+
+	reserved-memory {
+		#address-cells = <2>;
+		#size-cells = <2>;
+		ranges;
+		swiotlb: restricted_dma_reserved {
+			compatible = "restricted-dma-pool";
+			reg = <0xFFFFFFFF>;
+			size = <0xFFFFFFFF>;
+			alignment = <0xFFFFFFFF>;
+		};
+
+		dice {
+			compatible = "google,open-dice";
+			no-map;
+			reg = <0xFFFFFFFF>;
+		};
+	};
+
+	cpus {
+		#address-cells = <1>;
+		#size-cells = <0>;
+		cpu@0 {
+			device_type = "cpu";
+		};
+		cpu@1 {
+			device_type = "cpu";
+		    reg = <0x00 0x80000000 0xFFFFFFFF>;
+		};
+    };
+
+    rng@90000000 {
+        compatible = "android,rng";
+        reg = <0x0 0x9 0x0 0xFF>;
+        interrupts = <0x0 0xF 0x4>;
+        google,eh,ignore-gctrl-reset;
+        status = "okay";
+    };
+};
diff --git a/service_vm/comm/Android.bp b/service_vm/comm/Android.bp
index 3a18052..6e05587 100644
--- a/service_vm/comm/Android.bp
+++ b/service_vm/comm/Android.bp
@@ -43,3 +43,31 @@
         "std",
     ],
 }
+
+rust_defaults {
+    name: "libservice_vm_comm_test_defaults",
+    crate_name: "diced_open_dice_test",
+    srcs: ["tests/*.rs"],
+    test_suites: ["general-tests"],
+    prefer_rlib: true,
+    rustlibs: [
+        "libdiced_sample_inputs",
+        "libdiced_open_dice",
+    ],
+}
+
+rust_test {
+    name: "libservice_vm_comm.test",
+    defaults: ["libservice_vm_comm_test_defaults"],
+    rustlibs: [
+        "libservice_vm_comm",
+    ],
+}
+
+rust_test {
+    name: "libservice_vm_comm_nostd.test",
+    defaults: ["libservice_vm_comm_test_defaults"],
+    rustlibs: [
+        "libservice_vm_comm_nostd",
+    ],
+}
diff --git a/service_vm/comm/TEST_MAPPING b/service_vm/comm/TEST_MAPPING
new file mode 100644
index 0000000..e677ba2
--- /dev/null
+++ b/service_vm/comm/TEST_MAPPING
@@ -0,0 +1,12 @@
+// When adding or removing tests here, don't forget to amend _all_modules list in
+// wireless/android/busytown/ath_config/configs/prod/avf/tests.gcl
+{
+  "avf-presubmit" : [
+    {
+      "name" : "libservice_vm_comm.test"
+    },
+    {
+      "name" : "libservice_vm_comm_nostd.test"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/service_vm/comm/src/client_vm_csr.cddl b/service_vm/comm/src/client_vm_csr.cddl
new file mode 100644
index 0000000..bbc709a
--- /dev/null
+++ b/service_vm/comm/src/client_vm_csr.cddl
@@ -0,0 +1,62 @@
+; CDDL for the CSR sent from the client VM to the RKP VM for pVM remote attestation.
+
+Csr = [
+    DiceCertChain,      ; The DICE chain containing measurement of the client VM. See
+                        ; keymint/generateCertificateRequestV2.cddl for the DiceCertChain
+                        ; definition.
+    SignedData,
+]
+
+; COSE_Sign [RFC9052 s4.1]
+SignedData = [
+    protected: {},            ; The signing algorithms are specified in each signature
+                              ; separately.
+    unprotected: {},
+    payload: bstr .cbor CsrPayload,
+    Signatures,
+]
+
+CsrPayload = [                    ; CBOR Array defining the payload for CSR
+   challenge: bstr .size (0..64), ; The challenge is provided by the client server.
+                                  ; It will be included in the certificate chain in the
+                                  ; attestation result, serving as proof of the freshness
+                                  ; of the result.
+   PublicKey,                     ; COSE_Key encoded EC P-256 public key [ RFC9053 s7.1.1 ]
+                                  ; to be attested. See keymint/PublicKey.cddl for the
+                                  ; definition, the test flag `-70000` is never used.
+]
+
+Signatures = [
+    dice_cdi_leaf_signature: COSE_Signature_Dice_Cdi_Leaf,
+    attestation_key_signature: COSE_Signature_Attestation_Key,
+]
+
+; COSE_Signature [RFC9052 s4.1]
+COSE_Signature_Dice_Cdi_Leaf = [
+    protected: bstr .cbor { 1: AlgorithmEdDSA },
+    unprotected: {},
+    signature: bstr,                         ; Ed25519(CDI_Leaf_Priv, SigStruct)
+]
+
+; COSE_Signature [RFC9052 s4.1]
+COSE_Signature_Attestation_Key = [
+    protected: bstr .cbor { 1: AlgorithmES256 },
+    unprotected: {},
+    signature: bstr,                         ; ECDSA(PrivateKey, SigStruct)
+]
+
+; Sig_structure for SignedData [ RFC9052 s4.4 ]
+SigStruct = {
+    context: "Signature",
+    external_aad: bstr .size 0,
+    payload: bstr .cbor CsrPayload,
+}
+
+; ASN.1 DER-encoded EC P-256 ECPrivateKey [ RFC 5915 s3 ]:
+; ECPrivateKey ::= SEQUENCE {
+;     version        INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1),
+;     privateKey     OCTET STRING,
+;     parameters [0] ECParameters {{ NamedCurve }} OPTIONAL,
+;     publicKey  [1] BIT STRING OPTIONAL
+;}
+PrivateKey = bstr
diff --git a/service_vm/comm/src/csr.rs b/service_vm/comm/src/csr.rs
new file mode 100644
index 0000000..757d080
--- /dev/null
+++ b/service_vm/comm/src/csr.rs
@@ -0,0 +1,126 @@
+// Copyright 2023, The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! This module contains the structs related to the CSR (Certificate Signing Request)
+//! sent from the client VM to the service VM for attestation.
+
+use alloc::vec;
+use alloc::vec::Vec;
+use ciborium::Value;
+use coset::{self, CborSerializable, CoseError};
+use log::error;
+
+/// Represents a CSR sent from the client VM to the service VM for attestation.
+///
+/// See client_vm_csr.cddl for the definition of the CSR.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct Csr {
+    /// The DICE certificate chain of the client VM.
+    pub dice_cert_chain: Vec<u8>,
+
+    /// The signed CSR payload in COSE_Sign structure, which includes two signatures:
+    /// - one by CDI_Leaf_Priv of the client VM's DICE chain,
+    /// - another by the private key corresponding to the public key.
+    pub signed_csr_payload: Vec<u8>,
+}
+
+impl Csr {
+    /// Serializes this object to a CBOR-encoded vector.
+    pub fn into_cbor_vec(self) -> coset::Result<Vec<u8>> {
+        let value = Value::Array(vec![
+            Value::Bytes(self.dice_cert_chain),
+            Value::Bytes(self.signed_csr_payload),
+        ]);
+        value.to_vec()
+    }
+
+    /// Creates an object instance from the provided CBOR-encoded slice.
+    pub fn from_cbor_slice(data: &[u8]) -> coset::Result<Self> {
+        let value = Value::from_slice(data)?;
+        let Value::Array(mut arr) = value else {
+            return Err(CoseError::UnexpectedItem(cbor_value_type(&value), "array"));
+        };
+        if arr.len() != 2 {
+            return Err(CoseError::UnexpectedItem("array", "array with 2 items"));
+        }
+        Ok(Self {
+            signed_csr_payload: try_as_bytes(arr.remove(1), "signed_csr_payload")?,
+            dice_cert_chain: try_as_bytes(arr.remove(0), "dice_cert_chain")?,
+        })
+    }
+}
+
+/// Represents the data to be signed and sent from the client VM to the service VM
+/// for attestation.
+///
+/// It will be signed by both CDI_Leaf_Priv of the client VM's DICE chain and
+/// the private key corresponding to the public key to be attested.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct CsrPayload {
+    /// COSE_Key encoded EC P-256 public key to be attested.
+    pub public_key: Vec<u8>,
+
+    /// A random array with a length between 0 and 64.
+    /// It will be included in the certificate chain in the attestation result,
+    /// serving as proof of the freshness of the result.
+    pub challenge: Vec<u8>,
+}
+
+impl CsrPayload {
+    /// Serializes this object to a CBOR-encoded vector.
+    pub fn into_cbor_vec(self) -> coset::Result<Vec<u8>> {
+        let value = Value::Array(vec![Value::Bytes(self.public_key), Value::Bytes(self.challenge)]);
+        value.to_vec()
+    }
+
+    /// Creates an object instance from the provided CBOR-encoded slice.
+    pub fn from_cbor_slice(data: &[u8]) -> coset::Result<Self> {
+        let value = Value::from_slice(data)?;
+        let Value::Array(mut arr) = value else {
+            return Err(CoseError::UnexpectedItem(cbor_value_type(&value), "array"));
+        };
+        if arr.len() != 2 {
+            return Err(CoseError::UnexpectedItem("array", "array with 2 items"));
+        }
+        Ok(Self {
+            challenge: try_as_bytes(arr.remove(1), "challenge")?,
+            public_key: try_as_bytes(arr.remove(0), "public_key")?,
+        })
+    }
+}
+
+fn try_as_bytes(v: Value, context: &str) -> coset::Result<Vec<u8>> {
+    if let Value::Bytes(data) = v {
+        Ok(data)
+    } else {
+        let v_type = cbor_value_type(&v);
+        error!("The provided value type '{v_type}' is not of type 'bytes': {context}");
+        Err(CoseError::UnexpectedItem(v_type, "bytes"))
+    }
+}
+
+fn cbor_value_type(v: &Value) -> &'static str {
+    match v {
+        Value::Integer(_) => "int",
+        Value::Bytes(_) => "bstr",
+        Value::Float(_) => "float",
+        Value::Text(_) => "tstr",
+        Value::Bool(_) => "bool",
+        Value::Null => "nul",
+        Value::Tag(_, _) => "tag",
+        Value::Array(_) => "array",
+        Value::Map(_) => "map",
+        _ => "other",
+    }
+}
diff --git a/service_vm/comm/src/lib.rs b/service_vm/comm/src/lib.rs
index d8f7bd7..bb85a26 100644
--- a/service_vm/comm/src/lib.rs
+++ b/service_vm/comm/src/lib.rs
@@ -19,11 +19,13 @@
 
 extern crate alloc;
 
+mod csr;
 mod message;
 mod vsock;
 
+pub use csr::{Csr, CsrPayload};
 pub use message::{
-    EcdsaP256KeyPair, GenerateCertificateRequestParams, Request, RequestProcessingError, Response,
-    ServiceVmRequest,
+    ClientVmAttestationParams, EcdsaP256KeyPair, GenerateCertificateRequestParams, Request,
+    RequestProcessingError, Response, ServiceVmRequest,
 };
 pub use vsock::VmType;
diff --git a/service_vm/comm/src/message.rs b/service_vm/comm/src/message.rs
index f8d7420..6dd0ccd 100644
--- a/service_vm/comm/src/message.rs
+++ b/service_vm/comm/src/message.rs
@@ -50,6 +50,22 @@
     /// Creates a certificate signing request to be sent to the
     /// provisioning server.
     GenerateCertificateRequest(GenerateCertificateRequestParams),
+
+    /// Requests the service VM to attest the client VM and issue a certificate
+    /// if the attestation succeeds.
+    RequestClientVmAttestation(ClientVmAttestationParams),
+}
+
+/// Represents the params passed to `Request::RequestClientVmAttestation`.
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct ClientVmAttestationParams {
+    /// The CBOR-encoded CSR signed by the CDI_Leaf_Priv of the client VM's DICE chain
+    /// and the private key to be attested.
+    /// See client_vm_csr.cddl for the definition of the CSR.
+    pub csr: Vec<u8>,
+
+    /// The key blob retrieved from RKPD by virtualizationservice.
+    pub remotely_provisioned_key_blob: Vec<u8>,
 }
 
 /// Represents a response to a request sent to the service VM.
@@ -66,6 +82,11 @@
     /// Returns a CBOR Certificate Signing Request (Csr) serialized into a byte array.
     GenerateCertificateRequest(Vec<u8>),
 
+    /// Returns a certificate covering the public key to be attested in the provided CSR.
+    /// The certificate is signed by the remotely provisioned private key and also
+    /// includes an extension that describes the attested client VM.
+    RequestClientVmAttestation(Vec<u8>),
+
     /// Encountered an error during the request processing.
     Err(RequestProcessingError),
 }
@@ -93,6 +114,12 @@
 
     /// The DICE chain of the service VM is missing.
     MissingDiceChain,
+
+    /// Failed to decrypt the remotely provisioned key blob.
+    FailedToDecryptKeyBlob,
+
+    /// The requested operation has not been implemented.
+    OperationUnimplemented,
 }
 
 impl fmt::Display for RequestProcessingError {
@@ -109,6 +136,12 @@
                 write!(f, "An error happened when serializing to/from a CBOR Value.")
             }
             Self::MissingDiceChain => write!(f, "The DICE chain of the service VM is missing"),
+            Self::FailedToDecryptKeyBlob => {
+                write!(f, "Failed to decrypt the remotely provisioned key blob")
+            }
+            Self::OperationUnimplemented => {
+                write!(f, "The requested operation has not been implemented")
+            }
         }
     }
 }
diff --git a/service_vm/comm/tests/api_test.rs b/service_vm/comm/tests/api_test.rs
new file mode 100644
index 0000000..44a3ef9
--- /dev/null
+++ b/service_vm/comm/tests/api_test.rs
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+use diced_open_dice::DiceArtifacts;
+use service_vm_comm::{Csr, CsrPayload};
+
+/// The following test data are generated with urandom
+const DATA1: [u8; 32] = [
+    0x8b, 0x09, 0xc0, 0x7e, 0x20, 0x3c, 0xa2, 0x11, 0x7e, 0x7f, 0x0b, 0xdd, 0x2b, 0x68, 0x98, 0xb0,
+    0x2b, 0x34, 0xb5, 0x63, 0x39, 0x01, 0x90, 0x06, 0xaf, 0x5f, 0xdd, 0xb7, 0x81, 0xca, 0xc7, 0x46,
+];
+const DATA2: [u8; 16] = [
+    0x6c, 0xb9, 0x39, 0x86, 0x9b, 0x2f, 0x12, 0xd8, 0x45, 0x92, 0x57, 0x44, 0x65, 0xce, 0x94, 0x63,
+];
+
+#[test]
+fn csr_payload_cbor_serialization() {
+    let csr_payload = CsrPayload { public_key: DATA1.to_vec(), challenge: DATA2.to_vec() };
+    let expected_csr_payload = csr_payload.clone();
+    let cbor_vec = csr_payload.into_cbor_vec().unwrap();
+    let deserialized_csr_payload = CsrPayload::from_cbor_slice(&cbor_vec).unwrap();
+
+    assert_eq!(expected_csr_payload, deserialized_csr_payload);
+}
+
+#[test]
+fn csr_cbor_serialization() {
+    let dice_artifacts = diced_sample_inputs::make_sample_bcc_and_cdis().unwrap();
+    let dice_cert_chain = dice_artifacts.bcc().unwrap().to_vec();
+    let csr = Csr { signed_csr_payload: DATA1.to_vec(), dice_cert_chain };
+    let expected_csr = csr.clone();
+    let cbor_vec = csr.into_cbor_vec().unwrap();
+    let deserialized_csr = Csr::from_cbor_slice(&cbor_vec).unwrap();
+
+    assert_eq!(expected_csr, deserialized_csr);
+}
diff --git a/service_vm/requests/src/api.rs b/service_vm/requests/src/api.rs
index eae0370..315d2af 100644
--- a/service_vm/requests/src/api.rs
+++ b/service_vm/requests/src/api.rs
@@ -14,6 +14,7 @@
 
 //! This module contains the main API for the request processing module.
 
+use crate::client_vm;
 use crate::rkp;
 use alloc::vec::Vec;
 use diced_open_dice::DiceArtifacts;
@@ -31,6 +32,8 @@
             rkp::generate_certificate_request(p, dice_artifacts)
                 .map_or_else(Response::Err, Response::GenerateCertificateRequest)
         }
+        Request::RequestClientVmAttestation(p) => client_vm::request_attestation(p, dice_artifacts)
+            .map_or_else(Response::Err, Response::RequestClientVmAttestation),
     }
 }
 
diff --git a/service_vm/requests/src/client_vm.rs b/service_vm/requests/src/client_vm.rs
new file mode 100644
index 0000000..1081f3a
--- /dev/null
+++ b/service_vm/requests/src/client_vm.rs
@@ -0,0 +1,46 @@
+// Copyright 2023, The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! This module contains functions related to the attestation of the
+//! client VM.
+
+use crate::keyblob::decrypt_private_key;
+use alloc::vec::Vec;
+use core::result;
+use diced_open_dice::DiceArtifacts;
+use log::error;
+use service_vm_comm::{ClientVmAttestationParams, RequestProcessingError};
+
+type Result<T> = result::Result<T, RequestProcessingError>;
+
+pub(super) fn request_attestation(
+    params: ClientVmAttestationParams,
+    dice_artifacts: &dyn DiceArtifacts,
+) -> Result<Vec<u8>> {
+    // TODO(b/309440321): Verify the signatures in the csr.
+
+    // TODO(b/278717513): Compare client VM's DICE chain up to pvmfw cert with
+    // RKP VM's DICE chain.
+
+    let _private_key =
+        decrypt_private_key(&params.remotely_provisioned_key_blob, dice_artifacts.cdi_seal())
+            .map_err(|e| {
+                error!("Failed to decrypt the remotely provisioned key blob: {e}");
+                RequestProcessingError::FailedToDecryptKeyBlob
+            })?;
+
+    // TODO(b/309441500): Build a new certificate signed with the remotely provisioned
+    // private key.
+    Err(RequestProcessingError::OperationUnimplemented)
+}
diff --git a/service_vm/requests/src/keyblob.rs b/service_vm/requests/src/keyblob.rs
index 456c879..1fb7a67 100644
--- a/service_vm/requests/src/keyblob.rs
+++ b/service_vm/requests/src/keyblob.rs
@@ -20,8 +20,6 @@
 use core::result;
 use serde::{Deserialize, Serialize};
 use service_vm_comm::RequestProcessingError;
-// TODO(b/241428146): This will be used once the retrieval mechanism is available.
-#[cfg(test)]
 use zeroize::Zeroizing;
 
 type Result<T> = result::Result<T, RequestProcessingError>;
@@ -61,9 +59,6 @@
         EncryptedKeyBlobV1::new(private_key, kek_secret).map(Self::V1)
     }
 
-    // TODO(b/241428146): Use this function to decrypt the retrieved keyblob once the retrieval
-    // mechanism is available.
-    #[cfg(test)]
     pub(crate) fn decrypt_private_key(&self, kek_secret: &[u8]) -> Result<Zeroizing<Vec<u8>>> {
         match self {
             Self::V1(blob) => blob.decrypt_private_key(kek_secret),
@@ -85,7 +80,6 @@
         Ok(Self { kek_salt, encrypted_private_key: ciphertext.to_vec() })
     }
 
-    #[cfg(test)]
     fn decrypt_private_key(&self, kek_secret: &[u8]) -> Result<Zeroizing<Vec<u8>>> {
         let kek = hkdf::<32>(kek_secret, &self.kek_salt, KEK_INFO, Digester::sha512())?;
         let mut out = Zeroizing::new(vec![0u8; self.encrypted_private_key.len()]);
@@ -101,6 +95,15 @@
     }
 }
 
+pub(crate) fn decrypt_private_key(
+    encrypted_key_blob: &[u8],
+    kek_secret: &[u8],
+) -> Result<Zeroizing<Vec<u8>>> {
+    let key_blob: EncryptedKeyBlob = cbor_util::deserialize(encrypted_key_blob)?;
+    let private_key = key_blob.decrypt_private_key(kek_secret)?;
+    Ok(private_key)
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -127,8 +130,7 @@
     fn decrypting_keyblob_succeeds_with_the_same_kek() -> Result<()> {
         let encrypted_key_blob =
             cbor_util::serialize(&EncryptedKeyBlob::new(&TEST_KEY, &TEST_SECRET1)?)?;
-        let encrypted_key_blob: EncryptedKeyBlob = cbor_util::deserialize(&encrypted_key_blob)?;
-        let decrypted_key = encrypted_key_blob.decrypt_private_key(&TEST_SECRET1)?;
+        let decrypted_key = decrypt_private_key(&encrypted_key_blob, &TEST_SECRET1)?;
 
         assert_eq!(TEST_KEY, decrypted_key.as_slice());
         Ok(())
@@ -138,8 +140,7 @@
     fn decrypting_keyblob_fails_with_a_different_kek() -> Result<()> {
         let encrypted_key_blob =
             cbor_util::serialize(&EncryptedKeyBlob::new(&TEST_KEY, &TEST_SECRET1)?)?;
-        let encrypted_key_blob: EncryptedKeyBlob = cbor_util::deserialize(&encrypted_key_blob)?;
-        let err = encrypted_key_blob.decrypt_private_key(&TEST_SECRET2).unwrap_err();
+        let err = decrypt_private_key(&encrypted_key_blob, &TEST_SECRET2).unwrap_err();
 
         let expected_err: RequestProcessingError =
             Error::CallFailed(ApiName::EVP_AEAD_CTX_open, CipherError::BadDecrypt.into()).into();
diff --git a/service_vm/requests/src/lib.rs b/service_vm/requests/src/lib.rs
index e3c5794..b2db298 100644
--- a/service_vm/requests/src/lib.rs
+++ b/service_vm/requests/src/lib.rs
@@ -19,6 +19,7 @@
 extern crate alloc;
 
 mod api;
+mod client_vm;
 mod keyblob;
 mod pub_key;
 mod rkp;
diff --git a/service_vm/test_apk/src/main.rs b/service_vm/test_apk/src/main.rs
index 7d8416f..ba65aca 100644
--- a/service_vm/test_apk/src/main.rs
+++ b/service_vm/test_apk/src/main.rs
@@ -14,10 +14,20 @@
 
 //! Main executable of Service VM client for manual testing.
 
-use anyhow::Result;
+use anyhow::{anyhow, ensure, Result};
 use log::{error, info};
-use std::{ffi::c_void, panic};
-use vm_payload_bindgen::AVmPayload_requestAttestation;
+use std::{
+    ffi::{c_void, CStr},
+    panic,
+    ptr::{self, NonNull},
+    result,
+};
+use vm_payload_bindgen::{
+    attestation_status_t, AVmAttestationResult, AVmAttestationResult_free,
+    AVmAttestationResult_getCertificateAt, AVmAttestationResult_getCertificateCount,
+    AVmAttestationResult_getPrivateKey, AVmAttestationResult_resultToString,
+    AVmAttestationResult_sign, AVmPayload_requestAttestation,
+};
 
 /// Entry point of the Service VM client.
 #[allow(non_snake_case)]
@@ -40,38 +50,180 @@
 
 fn try_main() -> Result<()> {
     info!("Welcome to Service VM Client!");
+
+    let too_big_challenge = &[0u8; 66];
+    let res = AttestationResult::request_attestation(too_big_challenge);
+    ensure!(res.is_err());
+    let status = res.unwrap_err();
+    ensure!(
+        status == attestation_status_t::ATTESTATION_ERROR_INVALID_CHALLENGE,
+        "Unexpected status: {:?}",
+        status
+    );
+    info!("Status: {:?}", status_to_cstr(status));
+
     // The data below is only a placeholder generated randomly with urandom
     let challenge = &[
         0x6c, 0xad, 0x52, 0x50, 0x15, 0xe7, 0xf4, 0x1d, 0xa5, 0x60, 0x7e, 0xd2, 0x7d, 0xf1, 0x51,
         0x67, 0xc3, 0x3e, 0x73, 0x9b, 0x30, 0xbd, 0x04, 0x20, 0x2e, 0xde, 0x3b, 0x1d, 0xc8, 0x07,
         0x11, 0x7b,
     ];
-    info!("Sending challenge: {:?}", challenge);
-    let certificate = request_attestation(challenge);
-    info!("Certificate: {:?}", certificate);
+    let res = AttestationResult::request_attestation(challenge)
+        .map_err(|e| anyhow!("Unexpected status: {:?}", status_to_cstr(e)))?;
+
+    let cert_chain = res.certificate_chain()?;
+    info!("Attestation result certificateChain = {:?}", cert_chain);
+
+    let private_key = res.private_key()?;
+    info!("Attestation result privateKey = {:?}", private_key);
+
+    let message = b"Hello from Service VM client";
+    info!("Signing message: {:?}", message);
+    let signature = res.sign(message)?;
+    info!("Signature: {:?}", signature);
+
     Ok(())
 }
 
-fn request_attestation(challenge: &[u8]) -> Vec<u8> {
-    // SAFETY: It is safe as we only request the size of the certificate in this call.
-    let certificate_size = unsafe {
-        AVmPayload_requestAttestation(
-            challenge.as_ptr() as *const c_void,
-            challenge.len(),
-            [].as_mut_ptr(),
+#[derive(Debug)]
+struct AttestationResult(NonNull<AVmAttestationResult>);
+
+impl AttestationResult {
+    fn request_attestation(challenge: &[u8]) -> result::Result<Self, attestation_status_t> {
+        let mut res: *mut AVmAttestationResult = ptr::null_mut();
+        // SAFETY: It is safe as we only read the challenge within its bounds and the
+        // function does not retain any reference to it.
+        let status = unsafe {
+            AVmPayload_requestAttestation(
+                challenge.as_ptr() as *const c_void,
+                challenge.len(),
+                &mut res,
+            )
+        };
+        if status == attestation_status_t::ATTESTATION_OK {
+            info!("Attestation succeeds. Status: {:?}", status_to_cstr(status));
+            let res = NonNull::new(res).expect("The attestation result is null");
+            Ok(Self(res))
+        } else {
+            Err(status)
+        }
+    }
+
+    fn certificate_chain(&self) -> Result<Vec<Box<[u8]>>> {
+        let num_certs = get_certificate_count(self.as_ref());
+        let mut certs = Vec::with_capacity(num_certs);
+        for i in 0..num_certs {
+            certs.push(get_certificate_at(self.as_ref(), i)?);
+        }
+        Ok(certs)
+    }
+
+    fn private_key(&self) -> Result<Box<[u8]>> {
+        get_private_key(self.as_ref())
+    }
+
+    fn sign(&self, message: &[u8]) -> Result<Box<[u8]>> {
+        sign_with_attested_key(self.as_ref(), message)
+    }
+}
+
+impl AsRef<AVmAttestationResult> for AttestationResult {
+    fn as_ref(&self) -> &AVmAttestationResult {
+        // SAFETY: This field is private, and only populated with a successful call to
+        // `AVmPayload_requestAttestation`.
+        unsafe { self.0.as_ref() }
+    }
+}
+
+impl Drop for AttestationResult {
+    fn drop(&mut self) {
+        // SAFETY: This field is private, and only populated with a successful call to
+        // `AVmPayload_requestAttestation`, and not freed elsewhere.
+        unsafe { AVmAttestationResult_free(self.0.as_ptr()) };
+    }
+}
+
+fn get_certificate_count(res: &AVmAttestationResult) -> usize {
+    // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
+    // before getting freed.
+    unsafe { AVmAttestationResult_getCertificateCount(res) }
+}
+
+fn get_certificate_at(res: &AVmAttestationResult, index: usize) -> Result<Box<[u8]>> {
+    let size =
+        // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
+        // before getting freed.
+        unsafe { AVmAttestationResult_getCertificateAt(res, index, ptr::null_mut(), 0) };
+    let mut cert = vec![0u8; size];
+    // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
+    // before getting freed. This function only writes within the bounds of `cert`.
+    // And `cert` cannot overlap `res` because we just allocated it.
+    let size = unsafe {
+        AVmAttestationResult_getCertificateAt(
+            res,
+            index,
+            cert.as_mut_ptr() as *mut c_void,
+            cert.len(),
+        )
+    };
+    ensure!(size == cert.len());
+    Ok(cert.into_boxed_slice())
+}
+
+fn get_private_key(res: &AVmAttestationResult) -> Result<Box<[u8]>> {
+    let size =
+        // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
+        // before getting freed.
+        unsafe { AVmAttestationResult_getPrivateKey(res, ptr::null_mut(), 0) };
+    let mut private_key = vec![0u8; size];
+    // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
+    // before getting freed. This function only writes within the bounds of `private_key`.
+    // And `private_key` cannot overlap `res` because we just allocated it.
+    let size = unsafe {
+        AVmAttestationResult_getPrivateKey(
+            res,
+            private_key.as_mut_ptr() as *mut c_void,
+            private_key.len(),
+        )
+    };
+    ensure!(size == private_key.len());
+    Ok(private_key.into_boxed_slice())
+}
+
+fn sign_with_attested_key(res: &AVmAttestationResult, message: &[u8]) -> Result<Box<[u8]>> {
+    // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
+    // before getting freed.
+    let size = unsafe {
+        AVmAttestationResult_sign(
+            res,
+            message.as_ptr() as *const c_void,
+            message.len(),
+            ptr::null_mut(),
             0,
         )
     };
-    let mut certificate = vec![0u8; certificate_size];
-    // SAFETY: It is safe as we only write the data into the given buffer within the buffer
-    // size in this call.
-    unsafe {
-        AVmPayload_requestAttestation(
-            challenge.as_ptr() as *const c_void,
-            challenge.len(),
-            certificate.as_mut_ptr() as *mut c_void,
-            certificate.len(),
-        );
+    let mut signature = vec![0u8; size];
+    // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
+    // before getting freed. This function only writes within the bounds of `signature`.
+    // And `signature` cannot overlap `res` because we just allocated it.
+    let size = unsafe {
+        AVmAttestationResult_sign(
+            res,
+            message.as_ptr() as *const c_void,
+            message.len(),
+            signature.as_mut_ptr() as *mut c_void,
+            signature.len(),
+        )
     };
-    certificate
+    ensure!(size == signature.len());
+    Ok(signature.into_boxed_slice())
+}
+
+fn status_to_cstr(status: attestation_status_t) -> &'static CStr {
+    // SAFETY: The function only reads the given enum status and returns a pointer to a
+    // static string.
+    let message = unsafe { AVmAttestationResult_resultToString(status) };
+    // SAFETY: The pointer returned by `AVmAttestationResult_resultToString` is guaranteed to
+    // point to a valid C String.
+    unsafe { CStr::from_ptr(message) }
 }
diff --git a/virtualizationmanager/src/aidl.rs b/virtualizationmanager/src/aidl.rs
index da7dffe..bf00852 100644
--- a/virtualizationmanager/src/aidl.rs
+++ b/virtualizationmanager/src/aidl.rs
@@ -24,6 +24,7 @@
 use crate::selinux::{getfilecon, SeContext};
 use android_os_permissions_aidl::aidl::android::os::IPermissionController;
 use android_system_virtualizationcommon::aidl::android::system::virtualizationcommon::{
+    Certificate::Certificate,
     DeathReason::DeathReason,
     ErrorCode::ErrorCode,
 };
@@ -1029,6 +1030,7 @@
         .try_clone()
         .context("Failed to clone File from ParcelFileDescriptor")
         .or_binder_exception(ExceptionCode::BAD_PARCELABLE)
+        .map(File::from)
 }
 
 /// Converts an `&Option<ParcelFileDescriptor>` to an `Option<File>` by cloning the file.
@@ -1245,7 +1247,7 @@
         }
     }
 
-    fn requestAttestation(&self, csr: &[u8]) -> binder::Result<Vec<u8>> {
+    fn requestAttestation(&self, csr: &[u8]) -> binder::Result<Vec<Certificate>> {
         GLOBAL_SERVICE.requestAttestation(csr)
     }
 }
diff --git a/virtualizationmanager/src/composite.rs b/virtualizationmanager/src/composite.rs
index fe17ff4..a4b7eae 100644
--- a/virtualizationmanager/src/composite.rs
+++ b/virtualizationmanager/src/composite.rs
@@ -93,7 +93,8 @@
                 .context("Invalid partition image file descriptor")?
                 .as_ref()
                 .try_clone()
-                .context("Failed to clone partition image file descriptor")?;
+                .context("Failed to clone partition image file descriptor")?
+                .into();
             let path = fd_path_for_file(&file);
             let size = get_partition_size(&file, &path)?;
             files.push(file);
diff --git a/virtualizationmanager/src/selinux.rs b/virtualizationmanager/src/selinux.rs
index 0485943..ba62b7f 100644
--- a/virtualizationmanager/src/selinux.rs
+++ b/virtualizationmanager/src/selinux.rs
@@ -17,11 +17,10 @@
 use anyhow::{anyhow, bail, Context, Result};
 use std::ffi::{CStr, CString};
 use std::fmt;
-use std::fs::File;
 use std::io;
 use std::ops::Deref;
+use std::os::fd::AsRawFd;
 use std::os::raw::c_char;
-use std::os::unix::io::AsRawFd;
 use std::ptr;
 
 // Partially copied from system/security/keystore2/selinux/src/lib.rs
@@ -102,7 +101,7 @@
     }
 }
 
-pub fn getfilecon(file: &File) -> Result<SeContext> {
+pub fn getfilecon<F: AsRawFd>(file: &F) -> Result<SeContext> {
     let fd = file.as_raw_fd();
     let mut con: *mut c_char = ptr::null_mut();
     // SAFETY: the returned pointer `con` is wrapped in SeContext::Raw which is freed with
diff --git a/virtualizationservice/aidl/android/system/virtualizationcommon/Certificate.aidl b/virtualizationservice/aidl/android/system/virtualizationcommon/Certificate.aidl
new file mode 100644
index 0000000..d587541
--- /dev/null
+++ b/virtualizationservice/aidl/android/system/virtualizationcommon/Certificate.aidl
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.system.virtualizationcommon;
+
+/**
+ * This encodes a X.509 certificate returned in the pVM remote attestation.
+ */
+parcelable Certificate {
+    /**
+     * Contains the bytes of a DER-encoded X.509 certificate.
+     */
+    byte[] encodedCertificate;
+}
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
index 099a2c0..2592135 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
@@ -15,6 +15,7 @@
  */
 package android.system.virtualizationservice_internal;
 
+import android.system.virtualizationcommon.Certificate;
 import android.system.virtualizationservice.AssignableDevice;
 import android.system.virtualizationservice.VirtualMachineDebugInfo;
 import android.system.virtualizationservice_internal.AtomVmBooted;
@@ -62,7 +63,7 @@
      * @return A sequence of DER-encoded X.509 certificates that make up the attestation
      *         key's certificate chain. The attestation key is provided in the CSR.
      */
-    byte[] requestAttestation(in byte[] csr);
+    Certificate[] requestAttestation(in byte[] csr);
 
     /**
      * Get a list of assignable devices.
diff --git a/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl b/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl
index 87d3056..3c60478 100644
--- a/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl
+++ b/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl
@@ -15,6 +15,7 @@
  */
 package android.system.virtualmachineservice;
 
+import android.system.virtualizationcommon.Certificate;
 import android.system.virtualizationcommon.ErrorCode;
 
 /** {@hide} */
@@ -52,5 +53,5 @@
      * @return A sequence of DER-encoded X.509 certificates that make up the attestation
      *         key's certificate chain. The attestation key is provided in the CSR.
      */
-    byte[] requestAttestation(in byte[] csr);
+    Certificate[] requestAttestation(in byte[] csr);
 }
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index 4daa0cf..2be2b19 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -18,6 +18,7 @@
 use crate::atom::{forward_vm_booted_atom, forward_vm_creation_atom, forward_vm_exited_atom};
 use crate::rkpvm::request_attestation;
 use android_os_permissions_aidl::aidl::android::os::IPermissionController;
+use android_system_virtualizationcommon::aidl::android::system::virtualizationcommon::Certificate::Certificate;
 use android_system_virtualizationservice::{
     aidl::android::system::virtualizationservice::AssignableDevice::AssignableDevice,
     aidl::android::system::virtualizationservice::VirtualMachineDebugInfo::VirtualMachineDebugInfo,
@@ -158,7 +159,7 @@
         Ok(cids)
     }
 
-    fn requestAttestation(&self, csr: &[u8]) -> binder::Result<Vec<u8>> {
+    fn requestAttestation(&self, csr: &[u8]) -> binder::Result<Vec<Certificate>> {
         check_manage_access()?;
         info!("Received csr. Requestting attestation...");
         if cfg!(remote_attestation) {
diff --git a/virtualizationservice/src/rkpvm.rs b/virtualizationservice/src/rkpvm.rs
index 443b280..8f1de6b 100644
--- a/virtualizationservice/src/rkpvm.rs
+++ b/virtualizationservice/src/rkpvm.rs
@@ -17,18 +17,23 @@
 //! serves as a trusted platform to attest a client VM.
 
 use android_hardware_security_rkp::aidl::android::hardware::security::keymint::MacedPublicKey::MacedPublicKey;
+use android_system_virtualizationcommon::aidl::android::system::virtualizationcommon::Certificate::Certificate;
 use anyhow::{bail, Context, Result};
 use service_vm_comm::{GenerateCertificateRequestParams, Request, Response};
 use service_vm_manager::ServiceVm;
 
-pub(crate) fn request_attestation(csr: &[u8]) -> Result<Vec<u8>> {
+pub(crate) fn request_attestation(csr: &[u8]) -> Result<Vec<Certificate>> {
     let mut vm = ServiceVm::start()?;
 
     // TODO(b/271275206): Send the correct request type with client VM's
     // information to be attested.
     let request = Request::Reverse(csr.to_vec());
     match vm.process_request(request).context("Failed to process request")? {
-        Response::Reverse(cert) => Ok(cert),
+        // TODO(b/271275206): Adjust the response type.
+        Response::Reverse(cert) => {
+            let cert = Certificate { encodedCertificate: cert };
+            Ok(vec![cert])
+        }
         _ => bail!("Incorrect response type"),
     }
 }
diff --git a/virtualizationservice/vfio_handler/src/aidl.rs b/virtualizationservice/vfio_handler/src/aidl.rs
index 2968ff9..63f19c6 100644
--- a/virtualizationservice/vfio_handler/src/aidl.rs
+++ b/virtualizationservice/vfio_handler/src/aidl.rs
@@ -282,11 +282,13 @@
         .or_binder_exception(ExceptionCode::ILLEGAL_STATE)?;
     let buffer = read_values(dtbo_img_file, dt_size, entry.dt_offset.get().into())?;
 
-    let mut dtbo_fd = dtbo_fd
-        .as_ref()
-        .try_clone()
-        .context("Failed to clone File from ParcelFileDescriptor")
-        .or_binder_exception(ExceptionCode::BAD_PARCELABLE)?;
+    let mut dtbo_fd = File::from(
+        dtbo_fd
+            .as_ref()
+            .try_clone()
+            .context("Failed to create File from ParcelFileDescriptor")
+            .or_binder_exception(ExceptionCode::BAD_PARCELABLE)?,
+    );
 
     dtbo_fd
         .write_all(&buffer)
diff --git a/vm_payload/Android.bp b/vm_payload/Android.bp
index b5322a8..286612c 100644
--- a/vm_payload/Android.bp
+++ b/vm_payload/Android.bp
@@ -8,7 +8,7 @@
     crate_name: "vm_payload",
     defaults: ["avf_build_flags_rust"],
     visibility: ["//visibility:private"],
-    srcs: ["src/*.rs"],
+    srcs: ["src/lib.rs"],
     include_dirs: ["include"],
     prefer_rlib: true,
     rustlibs: [
@@ -19,11 +19,26 @@
         "liblazy_static",
         "liblibc",
         "liblog_rust",
+        "libopenssl",
         "librpcbinder_rs",
+        "libvm_payload_status_bindgen",
         "libvsock",
     ],
 }
 
+rust_bindgen {
+    name: "libvm_payload_status_bindgen",
+    wrapper_src: "include/vm_payload.h",
+    crate_name: "vm_payload_status_bindgen",
+    defaults: ["avf_build_flags_rust"],
+    source_stem: "bindings",
+    bindgen_flags: [
+        "--default-enum-style rust",
+        "--allowlist-type=attestation_status_t",
+    ],
+    visibility: [":__subpackages__"],
+}
+
 // Rust wrappers round the C API for Rust clients.
 // (Yes, this involves going Rust -> C -> Rust.)
 rust_bindgen {
@@ -33,6 +48,9 @@
     defaults: ["avf_build_flags_rust"],
     source_stem: "bindings",
     apex_available: ["com.android.compos"],
+    bindgen_flags: [
+        "--default-enum-style rust",
+    ],
     visibility: [
         "//packages/modules/Virtualization/compos",
         "//packages/modules/Virtualization/service_vm/test_apk",
@@ -49,6 +67,7 @@
         "libbinder_ndk",
         "libbinder_rpc_unstable",
         "liblog",
+        "libcrypto",
     ],
     whole_static_libs: ["libvm_payload_impl"],
     export_static_lib_headers: ["libvm_payload_impl"],
diff --git a/vm_payload/README.md b/vm_payload/README.md
index d5f5331..ec4dc59 100644
--- a/vm_payload/README.md
+++ b/vm_payload/README.md
@@ -8,8 +8,8 @@
 Note that only native code is supported in Microdroid, so no Java APIs are
 available in the VM, and only 64 bit code is supported.
 
-To create a VM and run the payload from Android, see
-[android.system.virtualmachine.VirtualMachineManager](https://android.googlesource.com/platform/packages/modules/Virtualization/+/refs/heads/master/javalib/src/android/system/virtualmachine/VirtualMachineManager.java).
+To create a VM and run the payload from Android see the [AVF Java
+APIs](../javalib/README.md).
 
 ## Entry point
 
diff --git a/vm_payload/include-restricted/vm_payload_restricted.h b/vm_payload/include-restricted/vm_payload_restricted.h
index ee92366..15c37ed 100644
--- a/vm_payload/include-restricted/vm_payload_restricted.h
+++ b/vm_payload/include-restricted/vm_payload_restricted.h
@@ -55,23 +55,4 @@
  */
 size_t AVmPayload_getDiceAttestationCdi(void* _Nullable data, size_t size);
 
-/**
- * Requests the remote attestation of the client VM.
- *
- * The challenge will be included in the certificate chain in the attestation result,
- * serving as proof of the freshness of the result.
- *
- * \param challenge A pointer to the challenge buffer.
- * \param challenge_size size of the challenge, the maximum supported challenge size is
- *                       64 bytes. An error will be returned if an invalid challenge is
- *                       passed.
- * \param buffer A pointer to the certificate buffer.
- * \param size number of bytes that can be written to the certificate buffer.
- *
- * \return the total size of the certificate
- */
-size_t AVmPayload_requestAttestation(const void* _Nonnull challenge, size_t challenge_size,
-                                     void* _Nullable buffer, size_t size)
-        __INTRODUCED_IN(__ANDROID_API_V__);
-
 __END_DECLS
diff --git a/vm_payload/include/vm_payload.h b/vm_payload/include/vm_payload.h
index c28cd42..951b57f 100644
--- a/vm_payload/include/vm_payload.h
+++ b/vm_payload/include/vm_payload.h
@@ -30,6 +30,30 @@
 typedef struct AIBinder AIBinder;
 
 /**
+ * Introduced in API 35.
+ * Remote attestation result if the attestation succeeds.
+ */
+struct AVmAttestationResult;
+
+/**
+ * Introduced in API 35.
+ * Remote attestation status types returned from remote attestation functions.
+ */
+typedef enum attestation_status_t : int32_t {
+    /** The remote attestation completes successfully. */
+    ATTESTATION_OK = 0,
+
+    /** The challenge size is not between 0 and 64. */
+    ATTESTATION_ERROR_INVALID_CHALLENGE = -10001,
+
+    /** Failed to attest the VM. Please retry at a later time. */
+    ATTESTATION_ERROR_ATTESTATION_FAILED = -10002,
+
+    /** Remote attestation is not supported in the current environment. */
+    ATTESTATION_ERROR_UNSUPPORTED = -10003,
+} attestation_status_t;
+
+/**
  * Notifies the host that the payload is ready.
  *
  * If the host app has set a `VirtualMachineCallback` for the VM, its
@@ -112,4 +136,129 @@
  */
 const char* _Nullable AVmPayload_getEncryptedStoragePath(void);
 
+/**
+ * Requests the remote attestation of the client VM.
+ *
+ * The challenge will be included in the certificate chain in the attestation result,
+ * serving as proof of the freshness of the result.
+ *
+ * \param challenge A pointer to the challenge buffer.
+ * \param challenge_size size of the challenge. The maximum supported challenge size is
+ *          64 bytes. The status ATTESTATION_ERROR_INVALID_CHALLENGE will be returned if
+ *          an invalid challenge is passed.
+ * \param result The remote attestation result will be filled here if the attestation
+ *               succeeds. The result remains valid until it is freed with
+ *              `AVmPayload_freeAttestationResult`.
+ *
+ * \return ATTESTATION_OK upon successful attestation.
+ */
+attestation_status_t AVmPayload_requestAttestation(
+        const void* _Nonnull challenge, size_t challenge_size,
+        struct AVmAttestationResult* _Nullable* _Nonnull result) __INTRODUCED_IN(__ANDROID_API_V__);
+
+/**
+ * Converts the return value from `AVmPayload_requestAttestation` to a text string
+ * representing the status code.
+ *
+ * \return a constant string value representing the status code. The string should not
+ * be deleted or freed by the application and remains valid for the lifetime of the VM.
+ */
+const char* _Nonnull AVmAttestationResult_resultToString(attestation_status_t status)
+        __INTRODUCED_IN(__ANDROID_API_V__);
+
+/**
+ * Frees all the data owned by the provided attestation result, including the result itself.
+ *
+ * Callers should ensure to invoke this API only once on a valid attestation result
+ * returned by `AVmPayload_requestAttestation` to avoid undefined behavior.
+ *
+ * \param result A pointer to the attestation result.
+ */
+void AVmAttestationResult_free(struct AVmAttestationResult* _Nullable result)
+        __INTRODUCED_IN(__ANDROID_API_V__);
+
+/**
+ * Reads the DER-encoded ECPrivateKey structure specified in [RFC 5915 s3] for the
+ * EC P-256 private key from the provided attestation result.
+ *
+ * \param result A pointer to the attestation result filled in
+ *              `AVmPayload_requestAttestation` when the attestation succeeds.
+ * \param data A pointer to the memory where the private key will be written
+ * (can be null if size is 0).
+ * \param size The maximum number of bytes that can be written to the data buffer.
+ * If `size` is smaller than the total size of the private key, the key data will be
+ * truncated to this `size`.
+ *
+ * \return The total size of the private key.
+ *
+ * [RFC 5915 s3]: https://datatracker.ietf.org/doc/html/rfc5915#section-3
+ */
+size_t AVmAttestationResult_getPrivateKey(const struct AVmAttestationResult* _Nonnull result,
+                                          void* _Nullable data, size_t size)
+        __INTRODUCED_IN(__ANDROID_API_V__);
+
+/**
+ * Signs the given message using ECDSA P-256, the message is first hashed with SHA-256 and
+ * then it is signed with the attested EC P-256 private key in the attestation result.
+ *
+ * \param result A pointer to the attestation result filled in
+ *              `AVmPayload_requestAttestation` when the attestation succeeds.
+ * \param message A pointer to the message buffer.
+ * \param message_size size of the message.
+ * \param data A pointer to the memory where the signature will be written
+ * (can be null if size is 0). The signature is a DER-encoded ECDSASignature structure
+ * detailed in the [RFC 6979].
+ * \param size The maximum number of bytes that can be written to the data buffer.
+ * If `size` is smaller than the total size of the signature, the signature will be
+ * truncated to this `size`.
+ *
+ * \return The total size of the signature.
+ *
+ * [RFC 6979]: https://datatracker.ietf.org/doc/html/rfc6979
+ */
+size_t AVmAttestationResult_sign(const struct AVmAttestationResult* _Nonnull result,
+                                 const void* _Nonnull message, size_t message_size,
+                                 void* _Nullable data, size_t size)
+        __INTRODUCED_IN(__ANDROID_API_V__);
+
+/**
+ * Gets the number of certificates in the certificate chain.
+ *
+ * The certificate chain consists of a sequence of DER-encoded X.509 certificates that form
+ * the attestation key's certificate chain. It starts with a root certificate and ends with a
+ * leaf certificate covering the attested public key.
+ *
+ * \param result A pointer to the attestation result obtained from `AVmPayload_requestAttestation`
+ *               when the attestation succeeds.
+ *
+ * \return The number of certificates in the certificate chain.
+ */
+size_t AVmAttestationResult_getCertificateCount(const struct AVmAttestationResult* _Nonnull result)
+        __INTRODUCED_IN(__ANDROID_API_V__);
+
+/**
+ * Retrieves the certificate at the given `index` from the certificate chain in the provided
+ * attestation result.
+ *
+ * The certificate chain consists of a sequence of DER-encoded X.509 certificates that form
+ * the attestation key's certificate chain. It starts with a root certificate and ends with a
+ * leaf certificate covering the attested public key.
+ *
+ * \param result A pointer to the attestation result obtained from `AVmPayload_requestAttestation`
+ *               when the attestation succeeds.
+ * \param index Index of the certificate to retrieve. The `index` must be within the range of
+ *              [0, number of certificates). The number of certificates can be obtained with
+ *              `AVmAttestationResult_getCertificateCount`.
+ * \param data A pointer to the memory where the certificate will be written
+ *             (can be null if size is 0).
+ * \param size The maximum number of bytes that can be written to the data buffer. If `size`
+ *             is smaller than the total size of the certificate, the certificate will be
+ *             truncated to this `size`.
+ *
+ * \return The total size of the certificate at the given `index`.
+ */
+size_t AVmAttestationResult_getCertificateAt(const struct AVmAttestationResult* _Nonnull result,
+                                             size_t index, void* _Nullable data, size_t size)
+        __INTRODUCED_IN(__ANDROID_API_V__);
+
 __END_DECLS
diff --git a/vm_payload/libvm_payload.map.txt b/vm_payload/libvm_payload.map.txt
index 32dd33b..975a5a3 100644
--- a/vm_payload/libvm_payload.map.txt
+++ b/vm_payload/libvm_payload.map.txt
@@ -8,6 +8,12 @@
     AVmPayload_getApkContentsPath;       # systemapi introduced=UpsideDownCake
     AVmPayload_getEncryptedStoragePath;  # systemapi introduced=UpsideDownCake
     AVmPayload_requestAttestation;       # systemapi introduced=VanillaIceCream
+    AVmAttestationResult_getPrivateKey;  # systemapi introduced=VanillaIceCream
+    AVmAttestationResult_sign;           # systemapi introduced=VanillaIceCream
+    AVmAttestationResult_free;           # systemapi introduced=VanillaIceCream
+    AVmAttestationResult_resultToString; # systemapi introduced=VanillaIceCream
+    AVmAttestationResult_getCertificateCount; # systemapi introduced=VanillaIceCream
+    AVmAttestationResult_getCertificateAt; # systemapi introduced=VanillaIceCream
   local:
     *;
 };
diff --git a/vm_payload/src/api.rs b/vm_payload/src/api.rs
index 93dbd1c..c76f2d3 100644
--- a/vm_payload/src/api.rs
+++ b/vm_payload/src/api.rs
@@ -14,20 +14,30 @@
 
 //! This module handles the interaction with virtual machine payload service.
 
-use android_system_virtualization_payload::aidl::android::system::virtualization::payload::IVmPayloadService::{
-    ENCRYPTEDSTORE_MOUNTPOINT, IVmPayloadService, VM_PAYLOAD_SERVICE_SOCKET_NAME, VM_APK_CONTENTS_PATH};
-use anyhow::{ensure, bail, Context, Result};
-use binder::{Strong, unstable_api::{AIBinder, new_spibinder}};
+use android_system_virtualization_payload::aidl::android::system::virtualization::payload:: IVmPayloadService::{
+    IVmPayloadService, ENCRYPTEDSTORE_MOUNTPOINT, VM_APK_CONTENTS_PATH,
+    VM_PAYLOAD_SERVICE_SOCKET_NAME, AttestationResult::AttestationResult,
+};
+use anyhow::{bail, ensure, Context, Result};
+use binder::{
+    unstable_api::{new_spibinder, AIBinder},
+    Strong, ExceptionCode,
+};
 use lazy_static::lazy_static;
 use log::{error, info, Level};
-use rpcbinder::{RpcSession, RpcServer};
+use rpcbinder::{RpcServer, RpcSession};
+use openssl::{ec::EcKey, sha::sha256, ecdsa::EcdsaSig};
 use std::convert::Infallible;
-use std::ffi::CString;
+use std::ffi::{CString, CStr};
 use std::fmt::Debug;
 use std::os::raw::{c_char, c_void};
 use std::path::Path;
-use std::ptr;
-use std::sync::{Mutex, atomic::{AtomicBool, Ordering}};
+use std::ptr::{self, NonNull};
+use std::sync::{
+    atomic::{AtomicBool, Ordering},
+    Mutex,
+};
+use vm_payload_status_bindgen::attestation_status_t;
 
 lazy_static! {
     static ref VM_APK_CONTENTS_PATH_C: CString =
@@ -263,42 +273,223 @@
 /// Behavior is undefined if any of the following conditions are violated:
 ///
 /// * `challenge` must be [valid] for reads of `challenge_size` bytes.
-/// * `buffer` must be [valid] for writes of `size` bytes. `buffer` can be null if `size` is 0.
+/// * `res` must be [valid] to write the attestation result.
+/// * The region of memory beginning at `challenge` with `challenge_size` bytes must not
+///  overlap with the region of memory `res` points to.
 ///
 /// [valid]: ptr#safety
 #[no_mangle]
 pub unsafe extern "C" fn AVmPayload_requestAttestation(
     challenge: *const u8,
     challenge_size: usize,
-    buffer: *mut u8,
+    res: &mut *mut AttestationResult,
+) -> attestation_status_t {
+    initialize_logging();
+    const MAX_CHALLENGE_SIZE: usize = 64;
+    if challenge_size > MAX_CHALLENGE_SIZE {
+        return attestation_status_t::ATTESTATION_ERROR_INVALID_CHALLENGE;
+    }
+    let challenge = if challenge_size == 0 {
+        &[]
+    } else {
+        // SAFETY: The caller guarantees that `challenge` is valid for reads of
+        // `challenge_size` bytes and `challenge_size` is not zero.
+        unsafe { std::slice::from_raw_parts(challenge, challenge_size) }
+    };
+    let service = unwrap_or_abort(get_vm_payload_service());
+    match service.requestAttestation(challenge) {
+        Ok(attestation_res) => {
+            *res = Box::into_raw(Box::new(attestation_res));
+            attestation_status_t::ATTESTATION_OK
+        }
+        Err(e) => {
+            error!("Remote attestation failed: {e:?}");
+            binder_status_to_attestation_status(e)
+        }
+    }
+}
+
+fn binder_status_to_attestation_status(status: binder::Status) -> attestation_status_t {
+    match status.exception_code() {
+        ExceptionCode::UNSUPPORTED_OPERATION => attestation_status_t::ATTESTATION_ERROR_UNSUPPORTED,
+        _ => attestation_status_t::ATTESTATION_ERROR_ATTESTATION_FAILED,
+    }
+}
+
+/// Converts the return value from `AVmPayload_requestAttestation` to a text string
+/// representing the error code.
+#[no_mangle]
+pub extern "C" fn AVmAttestationResult_resultToString(
+    status: attestation_status_t,
+) -> *const c_char {
+    let message = match status {
+        attestation_status_t::ATTESTATION_OK => {
+            CStr::from_bytes_with_nul(b"The remote attestation completes successfully.\0").unwrap()
+        }
+        attestation_status_t::ATTESTATION_ERROR_INVALID_CHALLENGE => {
+            CStr::from_bytes_with_nul(b"The challenge size is not between 0 and 64.\0").unwrap()
+        }
+        attestation_status_t::ATTESTATION_ERROR_ATTESTATION_FAILED => {
+            CStr::from_bytes_with_nul(b"Failed to attest the VM. Please retry at a later time.\0")
+                .unwrap()
+        }
+        attestation_status_t::ATTESTATION_ERROR_UNSUPPORTED => CStr::from_bytes_with_nul(
+            b"Remote attestation is not supported in the current environment.\0",
+        )
+        .unwrap(),
+    };
+    message.as_ptr()
+}
+
+/// Reads the DER-encoded ECPrivateKey structure specified in [RFC 5915 s3] for the
+/// EC P-256 private key from the provided attestation result.
+///
+/// # Safety
+///
+/// Behavior is undefined if any of the following conditions are violated:
+///
+/// * `data` must be [valid] for writes of `size` bytes, if size > 0.
+/// * The region of memory beginning at `data` with `size` bytes must not overlap with the
+///  region of memory `res` points to.
+///
+/// [valid]: ptr#safety
+/// [RFC 5915 s3]: https://datatracker.ietf.org/doc/html/rfc5915#section-3
+#[no_mangle]
+pub unsafe extern "C" fn AVmAttestationResult_getPrivateKey(
+    res: &AttestationResult,
+    data: *mut u8,
     size: usize,
 ) -> usize {
-    initialize_logging();
+    let private_key = &res.privateKey;
+    if size != 0 {
+        let data = NonNull::new(data).expect("data must not be null when size > 0");
+        // SAFETY: See the requirements on `data` above. The number of bytes copied doesn't exceed
+        // the length of either buffer, and the caller ensures that `private_key` cannot overlap
+        // `data`. We allow data to be null, which is never valid, but only if size == 0
+        // which is checked above.
+        unsafe {
+            ptr::copy_nonoverlapping(
+                private_key.as_ptr(),
+                data.as_ptr(),
+                std::cmp::min(private_key.len(), size),
+            )
+        };
+    }
+    private_key.len()
+}
 
-    // SAFETY: See the requirements on `challenge` above.
-    let challenge = unsafe { std::slice::from_raw_parts(challenge, challenge_size) };
-    let certificate = unwrap_or_abort(try_request_attestation(challenge));
+/// Signs the given message using ECDSA P-256, the message is first hashed with SHA-256 and
+/// then it is signed with the attested EC P-256 private key in the attestation result.
+///
+/// # Safety
+///
+/// Behavior is undefined if any of the following conditions are violated:
+///
+/// * `message` must be [valid] for reads of `message_size` bytes.
+/// * `data` must be [valid] for writes of `size` bytes, if size > 0.
+/// * The region of memory beginning at `data` with `size` bytes must not overlap with the
+///  region of memory `res` or `message` point to.
+///
+///
+/// [valid]: ptr#safety
+#[no_mangle]
+pub unsafe extern "C" fn AVmAttestationResult_sign(
+    res: &AttestationResult,
+    message: *const u8,
+    message_size: usize,
+    data: *mut u8,
+    size: usize,
+) -> usize {
+    if message_size == 0 {
+        panic!("Message to be signed must not be empty.")
+    }
+    // SAFETY: See the requirements on `message` above.
+    let message = unsafe { std::slice::from_raw_parts(message, message_size) };
+    let signature = unwrap_or_abort(try_ecdsa_sign(message, &res.privateKey));
+    if size != 0 {
+        let data = NonNull::new(data).expect("data must not be null when size > 0");
+        // SAFETY: See the requirements on `data` above. The number of bytes copied doesn't exceed
+        // the length of either buffer, and the caller ensures that `signature` cannot overlap
+        // `data`. We allow data to be null, which is never valid, but only if size == 0
+        // which is checked above.
+        unsafe {
+            ptr::copy_nonoverlapping(
+                signature.as_ptr(),
+                data.as_ptr(),
+                std::cmp::min(signature.len(), size),
+            )
+        };
+    }
+    signature.len()
+}
 
-    if size != 0 || buffer.is_null() {
-        // SAFETY: See the requirements on `buffer` above. The number of bytes copied doesn't exceed
-        // the length of either buffer, and `certificate` cannot overlap `buffer` because we just
-        // allocated it.
+fn try_ecdsa_sign(message: &[u8], der_encoded_ec_private_key: &[u8]) -> Result<Vec<u8>> {
+    let private_key = EcKey::private_key_from_der(der_encoded_ec_private_key)?;
+    let digest = sha256(message);
+    let sig = EcdsaSig::sign(&digest, &private_key)?;
+    Ok(sig.to_der()?)
+}
+
+/// Gets the number of certificates in the certificate chain.
+#[no_mangle]
+pub extern "C" fn AVmAttestationResult_getCertificateCount(res: &AttestationResult) -> usize {
+    res.certificateChain.len()
+}
+
+/// Retrieves the certificate at the given `index` from the certificate chain in the provided
+/// attestation result.
+///
+/// # Safety
+///
+/// Behavior is undefined if any of the following conditions are violated:
+///
+/// * `data` must be [valid] for writes of `size` bytes, if size > 0.
+/// * `index` must be within the range of [0, number of certificates). The number of certificates
+///   can be obtained with `AVmAttestationResult_getCertificateCount`.
+/// * The region of memory beginning at `data` with `size` bytes must not overlap with the
+///  region of memory `res` points to.
+///
+/// [valid]: ptr#safety
+#[no_mangle]
+pub unsafe extern "C" fn AVmAttestationResult_getCertificateAt(
+    res: &AttestationResult,
+    index: usize,
+    data: *mut u8,
+    size: usize,
+) -> usize {
+    let certificate =
+        &res.certificateChain.get(index).expect("The index is out of bounds.").encodedCertificate;
+    if size != 0 {
+        let data = NonNull::new(data).expect("data must not be null when size > 0");
+        // SAFETY: See the requirements on `data` above. The number of bytes copied doesn't exceed
+        // the length of either buffer, and the caller ensures that `certificate` cannot overlap
+        // `data`. We allow data to be null, which is never valid, but only if size == 0
+        // which is checked above.
         unsafe {
             ptr::copy_nonoverlapping(
                 certificate.as_ptr(),
-                buffer,
+                data.as_ptr(),
                 std::cmp::min(certificate.len(), size),
-            );
-        }
+            )
+        };
     }
     certificate.len()
 }
 
-fn try_request_attestation(challenge: &[u8]) -> Result<Vec<u8>> {
-    let certificate = get_vm_payload_service()?
-        .requestAttestation(challenge)
-        .context("Failed to request attestation")?;
-    Ok(certificate)
+/// Frees all the data owned by given attestation result and result itself.
+///
+/// # Safety
+///
+/// Behavior is undefined if any of the following conditions are violated:
+///
+/// * `res` must point to a valid `AttestationResult` and has not been freed before.
+#[no_mangle]
+pub unsafe extern "C" fn AVmAttestationResult_free(res: *mut AttestationResult) {
+    if !res.is_null() {
+        // SAFETY: The result is only freed once is ensured by the caller.
+        let res = unsafe { Box::from_raw(res) };
+        drop(res)
+    }
 }
 
 /// Gets the path to the APK contents.
diff --git a/vm_payload/src/lib.rs b/vm_payload/src/lib.rs
index 4d059d1..9e10895 100644
--- a/vm_payload/src/lib.rs
+++ b/vm_payload/src/lib.rs
@@ -17,7 +17,9 @@
 mod api;
 
 pub use api::{
-    AVmPayload_getCertificate, AVmPayload_getDiceAttestationCdi,
-    AVmPayload_getDiceAttestationChain, AVmPayload_getVmInstanceSecret,
-    AVmPayload_notifyPayloadReady,
+    AVmAttestationResult_free, AVmAttestationResult_getCertificateAt,
+    AVmAttestationResult_getCertificateCount, AVmAttestationResult_getPrivateKey,
+    AVmAttestationResult_resultToString, AVmAttestationResult_sign,
+    AVmPayload_getDiceAttestationCdi, AVmPayload_getDiceAttestationChain,
+    AVmPayload_getVmInstanceSecret, AVmPayload_notifyPayloadReady, AVmPayload_requestAttestation,
 };
diff --git a/vmbase/src/memory/dbm.rs b/vmbase/src/memory/dbm.rs
index 401022e..108cd5d 100644
--- a/vmbase/src/memory/dbm.rs
+++ b/vmbase/src/memory/dbm.rs
@@ -14,7 +14,7 @@
 
 //! Hardware management of the access flag and dirty state.
 
-use super::page_table::{is_leaf_pte, PageTable};
+use super::page_table::PageTable;
 use super::util::flush_region;
 use crate::{dsb, isb, read_sysreg, tlbi, write_sysreg};
 use aarch64_paging::paging::{Attributes, Descriptor, MemoryRegion};
@@ -52,14 +52,10 @@
 /// Flushes a memory range the descriptor refers to, if the descriptor is in writable-dirty state.
 pub(super) fn flush_dirty_range(
     va_range: &MemoryRegion,
-    desc: &mut Descriptor,
-    level: usize,
+    desc: &Descriptor,
+    _level: usize,
 ) -> Result<(), ()> {
-    // Only flush ranges corresponding to dirty leaf PTEs.
     let flags = desc.flags().ok_or(())?;
-    if !is_leaf_pte(&flags, level) {
-        return Ok(());
-    }
     if !flags.contains(Attributes::READ_ONLY) {
         flush_region(va_range.start().0, va_range.len());
     }
@@ -71,12 +67,9 @@
 pub(super) fn mark_dirty_block(
     va_range: &MemoryRegion,
     desc: &mut Descriptor,
-    level: usize,
+    _level: usize,
 ) -> Result<(), ()> {
     let flags = desc.flags().ok_or(())?;
-    if !is_leaf_pte(&flags, level) {
-        return Ok(());
-    }
     if flags.contains(Attributes::DBM) {
         assert!(flags.contains(Attributes::READ_ONLY), "unexpected PTE writable state");
         desc.modify_flags(Attributes::empty(), Attributes::READ_ONLY);
diff --git a/vmbase/src/memory/page_table.rs b/vmbase/src/memory/page_table.rs
index e067e96..dc346e7 100644
--- a/vmbase/src/memory/page_table.rs
+++ b/vmbase/src/memory/page_table.rs
@@ -16,7 +16,7 @@
 
 use crate::read_sysreg;
 use aarch64_paging::idmap::IdMap;
-use aarch64_paging::paging::{Attributes, MemoryRegion, PteUpdater};
+use aarch64_paging::paging::{Attributes, Constraints, Descriptor, MemoryRegion};
 use aarch64_paging::MapError;
 use core::result;
 
@@ -83,7 +83,9 @@
     /// code being currently executed. Otherwise, the Rust execution model (on which the borrow
     /// checker relies) would be violated.
     pub unsafe fn activate(&mut self) {
-        self.idmap.activate()
+        // SAFETY: the caller of this unsafe function asserts that switching to a different
+        // translation is safe
+        unsafe { self.idmap.activate() }
     }
 
     /// Maps the given range of virtual addresses to the physical addresses as lazily mapped
@@ -107,7 +109,15 @@
     /// Maps the given range of virtual addresses to the physical addresses as non-executable,
     /// read-only and writable-clean normal memory.
     pub fn map_data_dbm(&mut self, range: &MemoryRegion) -> Result<()> {
-        self.idmap.map_range(range, DATA_DBM)
+        // Map the region down to pages to minimize the size of the regions that will be marked
+        // dirty once a store hits them, but also to ensure that we can clear the read-only
+        // attribute while the mapping is live without causing break-before-make (BBM) violations.
+        // The latter implies that we must avoid the use of the contiguous hint as well.
+        self.idmap.map_range_with_constraints(
+            range,
+            DATA_DBM,
+            Constraints::NO_BLOCK_MAPPINGS | Constraints::NO_CONTIGUOUS_HINT,
+        )
     }
 
     /// Maps the given range of virtual addresses to the physical addresses as read-only
@@ -124,18 +134,20 @@
 
     /// Applies the provided updater function to a number of PTEs corresponding to a given memory
     /// range.
-    pub fn modify_range(&mut self, range: &MemoryRegion, f: &PteUpdater) -> Result<()> {
+    pub fn modify_range<F>(&mut self, range: &MemoryRegion, f: &F) -> Result<()>
+    where
+        F: Fn(&MemoryRegion, &mut Descriptor, usize) -> result::Result<(), ()>,
+    {
         self.idmap.modify_range(range, f)
     }
-}
 
-/// Checks whether a PTE at given level is a page or block descriptor.
-#[inline]
-pub(super) fn is_leaf_pte(flags: &Attributes, level: usize) -> bool {
-    const LEAF_PTE_LEVEL: usize = 3;
-    if flags.contains(Attributes::TABLE_OR_PAGE) {
-        level == LEAF_PTE_LEVEL
-    } else {
-        level < LEAF_PTE_LEVEL
+    /// Applies the provided callback function to a number of PTEs corresponding to a given memory
+    /// range.
+    pub fn walk_range<F>(&self, range: &MemoryRegion, f: &F) -> Result<()>
+    where
+        F: Fn(&MemoryRegion, &Descriptor, usize) -> result::Result<(), ()>,
+    {
+        let mut callback = |mr: &MemoryRegion, d: &Descriptor, l: usize| f(mr, d, l);
+        self.idmap.walk_range(range, &mut callback)
     }
 }
diff --git a/vmbase/src/memory/shared.rs b/vmbase/src/memory/shared.rs
index 6c8a844..dd433d4 100644
--- a/vmbase/src/memory/shared.rs
+++ b/vmbase/src/memory/shared.rs
@@ -16,12 +16,14 @@
 
 use super::dbm::{flush_dirty_range, mark_dirty_block, set_dbm_enabled};
 use super::error::MemoryTrackerError;
-use super::page_table::{is_leaf_pte, PageTable, MMIO_LAZY_MAP_FLAG};
+use super::page_table::{PageTable, MMIO_LAZY_MAP_FLAG};
 use super::util::{page_4kb_of, virt_to_phys};
 use crate::dsb;
 use crate::exceptions::HandleExceptionError;
 use crate::util::RangeExt as _;
-use aarch64_paging::paging::{Attributes, Descriptor, MemoryRegion as VaRange, VirtualAddress};
+use aarch64_paging::paging::{
+    Attributes, Descriptor, MemoryRegion as VaRange, VirtualAddress, BITS_PER_LEVEL, PAGE_SIZE,
+};
 use alloc::alloc::{alloc_zeroed, dealloc, handle_alloc_error};
 use alloc::boxed::Box;
 use alloc::vec::Vec;
@@ -253,7 +255,7 @@
         if get_mmio_guard().is_some() {
             for range in &self.mmio_regions {
                 self.page_table
-                    .modify_range(&get_va_range(range), &mmio_guard_unmap_page)
+                    .walk_range(&get_va_range(range), &mmio_guard_unmap_page)
                     .map_err(|_| MemoryTrackerError::FailedToUnmap)?;
             }
         }
@@ -319,14 +321,24 @@
     /// table entry and MMIO guard mapping the block. Breaks apart a block entry if required.
     fn handle_mmio_fault(&mut self, addr: VirtualAddress) -> Result<()> {
         let page_start = VirtualAddress(page_4kb_of(addr.0));
+        assert_eq!(page_start.0 % MMIO_GUARD_GRANULE_SIZE, 0);
         let page_range: VaRange = (page_start..page_start + MMIO_GUARD_GRANULE_SIZE).into();
         let mmio_guard = get_mmio_guard().unwrap();
+        // This must be safe and free from break-before-make (BBM) violations, given that the
+        // initial lazy mapping has the valid bit cleared, and each newly created valid descriptor
+        // created inside the mapping has the same size and alignment.
         self.page_table
-            .modify_range(&page_range, &verify_lazy_mapped_block)
+            .modify_range(&page_range, &|_: &VaRange, desc: &mut Descriptor, _: usize| {
+                let flags = desc.flags().expect("Unsupported PTE flags set");
+                if flags.contains(MMIO_LAZY_MAP_FLAG) && !flags.contains(Attributes::VALID) {
+                    desc.modify_flags(Attributes::VALID, Attributes::empty());
+                    Ok(())
+                } else {
+                    Err(())
+                }
+            })
             .map_err(|_| MemoryTrackerError::InvalidPte)?;
-        mmio_guard.map(page_start.0)?;
-        // Maps a single device page, breaking up block mappings if necessary.
-        self.page_table.map_device(&page_range).map_err(|_| MemoryTrackerError::FailedToMap)
+        Ok(mmio_guard.map(page_start.0)?)
     }
 
     /// Flush all memory regions marked as writable-dirty.
@@ -340,7 +352,7 @@
         // Now flush writable-dirty pages in those regions.
         for range in writable_regions.chain(self.payload_range.as_ref().into_iter()) {
             self.page_table
-                .modify_range(&get_va_range(range), &flush_dirty_range)
+                .walk_range(&get_va_range(range), &flush_dirty_range)
                 .map_err(|_| MemoryTrackerError::FlushRegionFailed)?;
         }
         Ok(())
@@ -467,33 +479,13 @@
     }
 }
 
-/// Checks whether block flags indicate it should be MMIO guard mapped.
-fn verify_lazy_mapped_block(
-    _range: &VaRange,
-    desc: &mut Descriptor,
-    level: usize,
-) -> result::Result<(), ()> {
-    let flags = desc.flags().expect("Unsupported PTE flags set");
-    if !is_leaf_pte(&flags, level) {
-        return Ok(()); // Skip table PTEs as they aren't tagged with MMIO_LAZY_MAP_FLAG.
-    }
-    if flags.contains(MMIO_LAZY_MAP_FLAG) && !flags.contains(Attributes::VALID) {
-        Ok(())
-    } else {
-        Err(())
-    }
-}
-
 /// MMIO guard unmaps page
 fn mmio_guard_unmap_page(
     va_range: &VaRange,
-    desc: &mut Descriptor,
+    desc: &Descriptor,
     level: usize,
 ) -> result::Result<(), ()> {
     let flags = desc.flags().expect("Unsupported PTE flags set");
-    if !is_leaf_pte(&flags, level) {
-        return Ok(());
-    }
     // This function will be called on an address range that corresponds to a device. Only if a
     // page has been accessed (written to or read from), will it contain the VALID flag and be MMIO
     // guard mapped. Therefore, we can skip unmapping invalid pages, they were never MMIO guard
@@ -503,9 +495,11 @@
             flags.contains(MMIO_LAZY_MAP_FLAG),
             "Attempting MMIO guard unmap for non-device pages"
         );
+        const MMIO_GUARD_GRANULE_SHIFT: u32 = MMIO_GUARD_GRANULE_SIZE.ilog2() - PAGE_SIZE.ilog2();
+        const MMIO_GUARD_GRANULE_LEVEL: usize =
+            3 - (MMIO_GUARD_GRANULE_SHIFT as usize / BITS_PER_LEVEL);
         assert_eq!(
-            va_range.len(),
-            MMIO_GUARD_GRANULE_SIZE,
+            level, MMIO_GUARD_GRANULE_LEVEL,
             "Failed to break down block mapping before MMIO guard mapping"
         );
         let page_base = va_range.start().0;
diff --git a/vmbase/src/util.rs b/vmbase/src/util.rs
index 7fe6015..25586bc 100644
--- a/vmbase/src/util.rs
+++ b/vmbase/src/util.rs
@@ -20,7 +20,12 @@
 #[macro_export]
 macro_rules! cstr {
     ($str:literal) => {{
-        core::ffi::CStr::from_bytes_with_nul(concat!($str, "\0").as_bytes()).unwrap()
+        const S: &str = concat!($str, "\0");
+        const C: &::core::ffi::CStr = match ::core::ffi::CStr::from_bytes_with_nul(S.as_bytes()) {
+            Ok(v) => v,
+            Err(_) => panic!("string contains interior NUL"),
+        };
+        C
     }};
 }