Merge "Allow host-controlled avf/untrusted prop" into main
diff --git a/README.md b/README.md
index 3935f93..210f70e 100644
--- a/README.md
+++ b/README.md
@@ -29,3 +29,4 @@
 * [Building and running a demo app in C++](demo_native/README.md)
 * [Debugging](docs/debug)
 * [Using custom VM](docs/custom_vm.md)
+* [Device assignment](docs/device_assignment.md)
\ No newline at end of file
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 4da96c8..f146b4e 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -12,6 +12,9 @@
       "name": "MicrodroidTestApp"
     },
     {
+      "name": "VmAttestationTestApp"
+    },
+    {
       "name": "CustomPvmfwHostTestCases"
     },
     {
diff --git a/docs/device_assignment.md b/docs/device_assignment.md
new file mode 100644
index 0000000..4c5b477
--- /dev/null
+++ b/docs/device_assignment.md
@@ -0,0 +1,239 @@
+# Getting started with device assignment
+
+Device assignment allows a VM to have direct access to HW without host/hyp
+intervention. AVF uses `vfio-platform` for device assignment, and host kernel
+support is required.
+
+This document explains how to setup and launch VM with device assignments.
+
+## VM device assignment DTBO (a.k.a. VM DTBO)
+
+For device assignment, a VM device assignment DTBO (a.k.a. VM DTBO) is required.
+VM DTBO is a device tree overlay which describes all assignable devices
+information. Information includes physical reg, IOMMU, device properties, and
+dependencies.
+
+VM DTBO allows to pass extra properties of assignable platform
+devices to the VM (which can't be discovered from the HW) while keeping the VMM
+device-agnostic.
+
+When the host boots, the bootloader provides VM DTBO to both Android and pvmfw.
+
+When a VM boots, the VMM selectively applies the DTBO based from provided
+labels, describing the assigned devices.
+
+## Prepare VM DTBO
+
+VM DTBO should be included in the dtbo partition. It should be in its own
+entry, and not together with any host OS's. See [DTB/DTBO Paritions] for
+partition format.
+
+[DTB/DTBO Paritions]: https://source.android.com/docs/core/architecture/dto/partitions
+
+### Write VM DTS for VM DTBO
+
+DTBO is compiled from device tree source (DTS) with `dtc` tool. [DTBO syntax]
+explains basic syntax of DTS.
+
+[DTBO syntax]: https://source.android.com/docs/core/architecture/dto/syntax
+
+Here are details and requirements:
+
+#### Describe assignable devices
+
+VM DTBO should describe assignable devices and their labels.
+
+* VM DTBO should have assignable devices in the `&{/}`, so it can be
+  overlaid onto VM DT. Assignable devices should be backed by physical device.
+  * We only support overlaying onto root node (i.e. `&{/}`) to prevent
+    unexpected modification of VM DT.
+* VM DTBO should have labels for assignable devices, so AVF can recognize
+  assignable device list. Labels should point to valid 'overlayable' nodes.
+  * Overlayable node is a node that would be applied to the base device tree
+    when DTBO is applied.
+
+#### Describe physical devices and physical IOMMUs
+
+VM DTBO should describe a `/host` node which describes physical devices and
+physical IOMMUs. The `/host` node only describes information for verification of
+assigned devices, and wouldn't be applied to VM DT. Here are details:
+
+* Physical IOMMU nodes
+  * IOMMU nodes must have a phandle to be referenced by a physical device node.
+  * IOMMU nodes must have `<android,pvmfw,token>` property. The property
+    describes the IOMMU token. An IOMMU token is a hypervisor-specific `<u64>`
+    which uniquely identifies a physical IOMMU. IOMMU token must be constant
+    across the VM boot for provisioning by pvmfw remains valid. The token must
+    be kept up-to-date across hypervisor updates.
+  * IOMMU nodes should be multi-master IOMMUs. (i.e. `#iommu-cells = <1>`)
+    * Other `#iommu-cells` values aren't supported for now.
+    * See: [Device tree binding for IOMMUs][IOMMU]
+* Physical device nodes
+  * Physical device nodes must have a `<android,pvmfw,target>` property that
+    references an overlayable node. The overlayable node contains the properties
+    that would be included in VM DT.
+  * Physical device nodes must have `<reg>` property to provide physical
+    regions.
+  * Physical device nodes can optionally contain `<iommus>` property. The
+    property is a prop-encoded-array and contains a number of
+    (iommu phandle, SID) pairs.
+    * IOMMU can be shared among devices, but should use distinct SIDs. Sharing
+      the same IOMMU-SID pair among multiple devices isn't supported for now.
+
+[IOMMU]: https://www.kernel.org/doc/Documentation/devicetree/bindings/iommu/iommu.txt
+
+#### Describe dependencies
+
+VM DTBO may have dependencies via phandle references. When a device node is
+assigned, dependencies of the node are also applied to VM DT.
+
+When dependencies are applied, siblings or children nodes of dependencies are
+ignored unless explicitly referenced.
+
+#### VM DTBO example
+
+Here's a simple example device tree source with four assignable devices nodes.
+
+```dts
+/dts-v1/;
+/plugin/;
+
+/ {
+    // host node describes physical devices and IOMMUs, and wouldn't be applied to VM DT
+    host {
+        #address-cells = <0x2>;
+        #size-cells = <0x1>;
+        rng {
+            reg = <0x0 0x12f00000 0x1000>;
+            iommus = <&iommu0 0x3>;
+            android,pvmfw,target = <&rng>;
+        };
+        light {
+            reg = <0x0 0x00f00000 0x1000>, <0x0 0x00f10000 0x1000>;
+            iommus = <&iommu1 0x4>, <&iommu2 0x5>;
+            android,pvmfw,target = <&light>;
+        };
+        led {
+            reg = <0x0 0x12000000 0x1000>;
+            iommus = <&iommu1 0x3>;
+            android,pvmfw,target = <&led>;
+        };
+        bus0 {
+            #address-cells = <0x1>;
+            #size-cells = <0x1>;
+            backlight {
+                reg = <0x300 0x100>;
+                android,pvmfw,target = <&backlight>;
+            };
+        };
+        iommu0: iommu0 {
+            #iommu-cells = <0x1>;
+            android,pvmfw,token = <0x0 0x12e40000>;
+        };
+        iommu1: iommu1 {
+            #iommu-cells = <0x1>;
+            android,pvmfw,token = <0x0 0x40000>;
+        };
+        iommu2: iommu2 {
+            #iommu-cells = <0x1>;
+            android,pvmfw,token = <0x0 0x50000>;
+        };
+    };
+};
+
+// Beginning of the assignable devices. Assigned devices would be applied to VM DT
+&{/} {  // We only allows to overlay to root node
+    rng: rng {
+        compatible = "android,rng";
+        android,rng,ignore-gctrl-reset;
+    };
+    light: light {
+        compatible = "android,light";
+        version = <0x1 0x2>;
+    };
+    led: led {
+        compatible = "android,led";
+        prop = <0x555>;
+    };
+    bus0 {
+        backlight: backlight {
+            compatible = "android,backlight";
+            android,backlight,ignore-gctrl-reset;
+        };
+    };
+};
+```
+
+If you compile the above with `dtc -@`, then you'll get `__symbols__` for free.
+The generated `__symbols__` indicates that there are four assignable devices.
+
+```dts
+    // generated __symbols__. AVF will ignore non-overlayable nodes.
+    __symbols__ {
+        iommu0 = "/host/iommu0";
+        iommu1 = "/host/iommu1";
+        iommu2 = "/host/iommu2";
+        rng = "/fragment@rng/__overlay__/rng";
+        light = "/fragment@sensor/__overlay__/light";
+        led = "/fragment@led/__overlay__/led";
+        backlight = "/fragment@backlight/__overlay__/bus0/backlight";
+    };
+```
+
+## Prepare AVF assignable devices XML
+
+AVF requires assignable device information to unbind from the host device driver
+and bind to VFIO driver. The information should be provided in an XML file at
+`/vendor/etc/avf/assignable_devices.xml`.
+
+Here's example.
+
+```xml
+<devices>
+    <device>
+        <kind>sensor</kind>
+        <dtbo_label>light</dtbo_label>
+        <sysfs_path>/sys/bus/platform/devices/16d00000.light</sysfs_path>
+    </device>
+</devices>
+```
+
+* `<kind>`: Device kind. Currently only used for debugging purposes and not used
+  for device assignment.
+* `<dtbo_label>`: Label in the VM DTBO (i.e. symbols in `__symbols__`). Must be
+  unique.
+* `<sysfs_path>`: Sysfs path of the device in host, used to bind to the VFIO
+  driver. Must be unique in the XML.
+
+## Boot with VM DTBO
+
+Bootloader should provide VM DTBO to both Android and pvmfw.
+
+### Provide VM DTBO index in dtbo.img
+
+Bootloader should provide the VM DTBO index with sysprop
+`ro.boot.hypervisor.vm_dtbo_idx.`. DTBO index represents DTBO location in
+dtbo.img.
+
+### Provide VM DTBO in the pvmfw config
+
+For protected VM, bootloader must provide VM DTBO to the pvmfw. pvmfw sanitizes
+incoming device tree with the VM DTBO.
+
+For more detail about providing VM DTBO in pvmfw,
+see: [pvmfw/README.md](../pvmfw/README.md#configuration-data-format)
+
+
+## Launch VM with device assignment
+
+We don't support client API yet in Android V, but you can use CLI to test device
+assignment. Note that host kernel support is required.
+
+Specify `--devices ${sysfs_path}` when booting VM. The parameter can be repeated
+multiple times for specifying multiple devices.
+
+Here's an example:
+
+```sh
+adb shell /apex/com.android.virt/bin/vm run-microdroid --devices /sys/bus/platform/devices/16d00000.light
+```
\ No newline at end of file
diff --git a/javalib/api/test-current.txt b/javalib/api/test-current.txt
index 5aff93f..3ea50e2 100644
--- a/javalib/api/test-current.txt
+++ b/javalib/api/test-current.txt
@@ -2,6 +2,7 @@
 package android.system.virtualmachine {
 
   public class VirtualMachine implements java.lang.AutoCloseable {
+    method @FlaggedApi("RELEASE_AVF_ENABLE_REMOTE_ATTESTATION") @RequiresPermission(android.system.virtualmachine.VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION) public void enableTestAttestation() throws android.system.virtualmachine.VirtualMachineException;
     method @NonNull @WorkerThread public java.io.OutputStream getConsoleInput() throws android.system.virtualmachine.VirtualMachineException;
     method @NonNull public java.io.File getRootDir();
   }
@@ -26,6 +27,7 @@
     method @RequiresPermission(android.system.virtualmachine.VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION) public boolean isFeatureEnabled(String) throws android.system.virtualmachine.VirtualMachineException;
     field public static final String FEATURE_DICE_CHANGES = "com.android.kvm.DICE_CHANGES";
     field public static final String FEATURE_MULTI_TENANT = "com.android.kvm.MULTI_TENANT";
+    field public static final String FEATURE_REMOTE_ATTESTATION = "com.android.kvm.REMOTE_ATTESTATION";
     field public static final String FEATURE_VENDOR_MODULES = "com.android.kvm.VENDOR_MODULES";
   }
 
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachine.java b/javalib/src/android/system/virtualmachine/VirtualMachine.java
index 5025e88..b4ba00b 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachine.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachine.java
@@ -42,6 +42,7 @@
 
 import static java.util.Objects.requireNonNull;
 
+import android.annotation.FlaggedApi;
 import android.annotation.CallbackExecutor;
 import android.annotation.IntDef;
 import android.annotation.IntRange;
@@ -1202,6 +1203,28 @@
     }
 
     /**
+     * Enables the VM to request attestation in testing mode.
+     *
+     * <p>This function provisions a key pair for the VM attestation testing, a fake certificate
+     * will be associated to the fake key pair when the VM requests attestation in testing mode.
+     *
+     * <p>The provisioned key pair can only be used in subsequent calls to {@link
+     * AVmPayload_requestAttestationForTesting} within a running VM.
+     *
+     * @hide
+     */
+    @TestApi
+    @RequiresPermission(USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION)
+    @FlaggedApi("RELEASE_AVF_ENABLE_REMOTE_ATTESTATION")
+    public void enableTestAttestation() throws VirtualMachineException {
+        try {
+            mVirtualizationService.getBinder().enableTestAttestation();
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        }
+    }
+
+    /**
      * Captures the current state of the VM in a {@link VirtualMachineDescriptor} instance. The VM
      * needs to be stopped to avoid inconsistency in its state representation.
      *
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineManager.java b/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
index 1607c0a..1a4b53a 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
@@ -116,7 +116,12 @@
     @Retention(RetentionPolicy.SOURCE)
     @StringDef(
             prefix = "FEATURE_",
-            value = {FEATURE_DICE_CHANGES, FEATURE_MULTI_TENANT, FEATURE_VENDOR_MODULES})
+            value = {
+                FEATURE_DICE_CHANGES,
+                FEATURE_MULTI_TENANT,
+                FEATURE_REMOTE_ATTESTATION,
+                FEATURE_VENDOR_MODULES
+            })
     public @interface Features {}
 
     /**
@@ -136,6 +141,15 @@
     public static final String FEATURE_MULTI_TENANT = IVirtualizationService.FEATURE_MULTI_TENANT;
 
     /**
+     * Feature to allow remote attestation in Microdroid.
+     *
+     * @hide
+     */
+    @TestApi
+    public static final String FEATURE_REMOTE_ATTESTATION =
+            IVirtualizationService.FEATURE_REMOTE_ATTESTATION;
+
+    /**
      * Feature to allow vendor modules in Microdroid.
      *
      * @hide
diff --git a/libs/libfdt/src/lib.rs b/libs/libfdt/src/lib.rs
index ab3c83f..d1ab24e 100644
--- a/libs/libfdt/src/lib.rs
+++ b/libs/libfdt/src/lib.rs
@@ -459,7 +459,7 @@
     }
 
     /// Returns the first subnode of this
-    pub fn first_subnode(&'a mut self) -> Result<Option<Self>> {
+    pub fn first_subnode(self) -> Result<Option<Self>> {
         let offset = self.fdt.first_subnode(self.offset)?;
 
         Ok(offset.map(|offset| Self { fdt: self.fdt, offset }))
diff --git a/libs/libfdt/tests/api_test.rs b/libs/libfdt/tests/api_test.rs
index cafbf97..ddc4538 100644
--- a/libs/libfdt/tests/api_test.rs
+++ b/libs/libfdt/tests/api_test.rs
@@ -378,7 +378,7 @@
     let mut data = fs::read(TEST_TREE_PHANDLE_PATH).unwrap();
     let fdt = Fdt::from_mut_slice(&mut data).unwrap();
 
-    let mut root = fdt.root_mut().unwrap();
+    let root = fdt.root_mut().unwrap();
     let mut subnode_iter = root.first_subnode().unwrap();
 
     while let Some(subnode) = subnode_iter {
diff --git a/microdroid_manager/aidl/android/system/virtualization/payload/IVmPayloadService.aidl b/microdroid_manager/aidl/android/system/virtualization/payload/IVmPayloadService.aidl
index 4813b35..b7a539b 100644
--- a/microdroid_manager/aidl/android/system/virtualization/payload/IVmPayloadService.aidl
+++ b/microdroid_manager/aidl/android/system/virtualization/payload/IVmPayloadService.aidl
@@ -107,9 +107,13 @@
      * serving as proof of the freshness of the result.
      *
      * @param challenge the maximum supported challenge size is 64 bytes.
+     * @param testMode whether the attestation is only for testing purposes. If testMode is true,
+     * caller must invoke {@link VirtualMachineManager#enableTestAttestation} prior to
+     * calling this method to provision a key pair to sign the attested result, and the returned
+     * certificate chain will not be RKP server rooted.
      *
      * @return An {@link AttestationResult} parcelable containing an attested key pair and its
      *         certification chain.
      */
-    AttestationResult requestAttestation(in byte[] challenge);
+    AttestationResult requestAttestation(in byte[] challenge, in boolean testMode);
 }
diff --git a/microdroid_manager/src/vm_payload_service.rs b/microdroid_manager/src/vm_payload_service.rs
index 20a1b89..959197a 100644
--- a/microdroid_manager/src/vm_payload_service.rs
+++ b/microdroid_manager/src/vm_payload_service.rs
@@ -68,7 +68,11 @@
         Ok(self.secret.dice_artifacts().cdi_attest().to_vec())
     }
 
-    fn requestAttestation(&self, challenge: &[u8]) -> binder::Result<AttestationResult> {
+    fn requestAttestation(
+        &self,
+        challenge: &[u8],
+        test_mode: bool,
+    ) -> binder::Result<AttestationResult> {
         self.check_restricted_apis_allowed()?;
         let ClientVmAttestationData { private_key, csr } =
             generate_attestation_key_and_csr(challenge, self.secret.dice_artifacts())
@@ -88,7 +92,7 @@
                 )
             })
             .with_log()?;
-        let cert_chain = self.virtual_machine_service.requestAttestation(&csr)?;
+        let cert_chain = self.virtual_machine_service.requestAttestation(&csr, test_mode)?;
         Ok(AttestationResult {
             privateKey: private_key.as_slice().to_vec(),
             certificateChain: cert_chain,
diff --git a/pvmfw/README.md b/pvmfw/README.md
index d7884fb..2758a5d 100644
--- a/pvmfw/README.md
+++ b/pvmfw/README.md
@@ -195,7 +195,8 @@
 
 In version 1.1, a third blob is added.
 
-- entry 2 may point to a [DTBO] that describes VM DTBO for device assignment.
+- entry 2 may point to a [DTBO] that describes VM DA DTBO for
+  [device assignment][device_assignment].
   pvmfw will provision assigned devices with the VM DTBO.
 
 In version 1.2, a fourth blob is added.
@@ -225,6 +226,7 @@
 [header]: src/config.rs
 [DTBO]: https://android.googlesource.com/platform/external/dtc/+/refs/heads/main/Documentation/dt-object-internal.txt
 [debug_policy]: ../docs/debug/README.md#debug-policy
+[device_assignment]: ../docs/device_assignment.md
 [secretkeeper_key]: https://android.googlesource.com/platform/system/secretkeeper/+/refs/heads/main/README.md#secretkeeper-public-key
 [vendor_hashtree_digest]: ../microdroid/README.md#verification-of-vendor-image
 
diff --git a/pvmfw/platform.dts b/pvmfw/platform.dts
index 9abc123..cea1c33 100644
--- a/pvmfw/platform.dts
+++ b/pvmfw/platform.dts
@@ -57,96 +57,497 @@
 			compatible = "arm,arm-v8";
 			enable-method = "psci";
 			reg = <0>;
+			operating-points-v2 = <&opp_table0>;
+			opp_table0: opp-table-0 {
+				compatible = "operating-points-v2";
+
+				opp1 { opp-hz = <PLACEHOLDER2>; };
+				opp2 { opp-hz = <PLACEHOLDER2>; };
+				opp3 { opp-hz = <PLACEHOLDER2>; };
+				opp4 { opp-hz = <PLACEHOLDER2>; };
+				opp5 { opp-hz = <PLACEHOLDER2>; };
+				opp6 { opp-hz = <PLACEHOLDER2>; };
+				opp7 { opp-hz = <PLACEHOLDER2>; };
+				opp8 { opp-hz = <PLACEHOLDER2>; };
+				opp9 { opp-hz = <PLACEHOLDER2>; };
+				opp10 { opp-hz = <PLACEHOLDER2>; };
+				opp11 { opp-hz = <PLACEHOLDER2>; };
+				opp12 { opp-hz = <PLACEHOLDER2>; };
+				opp13 { opp-hz = <PLACEHOLDER2>; };
+				opp14 { opp-hz = <PLACEHOLDER2>; };
+				opp15 { opp-hz = <PLACEHOLDER2>; };
+				opp16 { opp-hz = <PLACEHOLDER2>; };
+				opp17 { opp-hz = <PLACEHOLDER2>; };
+				opp18 { opp-hz = <PLACEHOLDER2>; };
+				opp19 { opp-hz = <PLACEHOLDER2>; };
+				opp20 { opp-hz = <PLACEHOLDER2>; };
+			};
 		};
 		cpu@1 {
 			device_type = "cpu";
 			compatible = "arm,arm-v8";
 			enable-method = "psci";
 			reg = <1>;
+			operating-points-v2 = <&opp_table1>;
+			opp_table1: opp-table-1 {
+				compatible = "operating-points-v2";
+
+				opp1 { opp-hz = <PLACEHOLDER2>; };
+				opp2 { opp-hz = <PLACEHOLDER2>; };
+				opp3 { opp-hz = <PLACEHOLDER2>; };
+				opp4 { opp-hz = <PLACEHOLDER2>; };
+				opp5 { opp-hz = <PLACEHOLDER2>; };
+				opp6 { opp-hz = <PLACEHOLDER2>; };
+				opp7 { opp-hz = <PLACEHOLDER2>; };
+				opp8 { opp-hz = <PLACEHOLDER2>; };
+				opp9 { opp-hz = <PLACEHOLDER2>; };
+				opp10 { opp-hz = <PLACEHOLDER2>; };
+				opp11 { opp-hz = <PLACEHOLDER2>; };
+				opp12 { opp-hz = <PLACEHOLDER2>; };
+				opp13 { opp-hz = <PLACEHOLDER2>; };
+				opp14 { opp-hz = <PLACEHOLDER2>; };
+				opp15 { opp-hz = <PLACEHOLDER2>; };
+				opp16 { opp-hz = <PLACEHOLDER2>; };
+				opp17 { opp-hz = <PLACEHOLDER2>; };
+				opp18 { opp-hz = <PLACEHOLDER2>; };
+				opp19 { opp-hz = <PLACEHOLDER2>; };
+				opp20 { opp-hz = <PLACEHOLDER2>; };
+			};
 		};
 		cpu@2 {
 			device_type = "cpu";
 			compatible = "arm,arm-v8";
 			enable-method = "psci";
 			reg = <2>;
+			operating-points-v2 = <&opp_table2>;
+			opp_table2: opp-table-2 {
+				compatible = "operating-points-v2";
+
+				opp1 { opp-hz = <PLACEHOLDER2>; };
+				opp2 { opp-hz = <PLACEHOLDER2>; };
+				opp3 { opp-hz = <PLACEHOLDER2>; };
+				opp4 { opp-hz = <PLACEHOLDER2>; };
+				opp5 { opp-hz = <PLACEHOLDER2>; };
+				opp6 { opp-hz = <PLACEHOLDER2>; };
+				opp7 { opp-hz = <PLACEHOLDER2>; };
+				opp8 { opp-hz = <PLACEHOLDER2>; };
+				opp9 { opp-hz = <PLACEHOLDER2>; };
+				opp10 { opp-hz = <PLACEHOLDER2>; };
+				opp11 { opp-hz = <PLACEHOLDER2>; };
+				opp12 { opp-hz = <PLACEHOLDER2>; };
+				opp13 { opp-hz = <PLACEHOLDER2>; };
+				opp14 { opp-hz = <PLACEHOLDER2>; };
+				opp15 { opp-hz = <PLACEHOLDER2>; };
+				opp16 { opp-hz = <PLACEHOLDER2>; };
+				opp17 { opp-hz = <PLACEHOLDER2>; };
+				opp18 { opp-hz = <PLACEHOLDER2>; };
+				opp19 { opp-hz = <PLACEHOLDER2>; };
+				opp20 { opp-hz = <PLACEHOLDER2>; };
+			};
 		};
 		cpu@3 {
 			device_type = "cpu";
 			compatible = "arm,arm-v8";
 			enable-method = "psci";
 			reg = <3>;
+			operating-points-v2 = <&opp_table3>;
+			opp_table3: opp-table-3 {
+				compatible = "operating-points-v2";
+
+				opp1 { opp-hz = <PLACEHOLDER2>; };
+				opp2 { opp-hz = <PLACEHOLDER2>; };
+				opp3 { opp-hz = <PLACEHOLDER2>; };
+				opp4 { opp-hz = <PLACEHOLDER2>; };
+				opp5 { opp-hz = <PLACEHOLDER2>; };
+				opp6 { opp-hz = <PLACEHOLDER2>; };
+				opp7 { opp-hz = <PLACEHOLDER2>; };
+				opp8 { opp-hz = <PLACEHOLDER2>; };
+				opp9 { opp-hz = <PLACEHOLDER2>; };
+				opp10 { opp-hz = <PLACEHOLDER2>; };
+				opp11 { opp-hz = <PLACEHOLDER2>; };
+				opp12 { opp-hz = <PLACEHOLDER2>; };
+				opp13 { opp-hz = <PLACEHOLDER2>; };
+				opp14 { opp-hz = <PLACEHOLDER2>; };
+				opp15 { opp-hz = <PLACEHOLDER2>; };
+				opp16 { opp-hz = <PLACEHOLDER2>; };
+				opp17 { opp-hz = <PLACEHOLDER2>; };
+				opp18 { opp-hz = <PLACEHOLDER2>; };
+				opp19 { opp-hz = <PLACEHOLDER2>; };
+				opp20 { opp-hz = <PLACEHOLDER2>; };
+			};
 		};
 		cpu@4 {
 			device_type = "cpu";
 			compatible = "arm,arm-v8";
 			enable-method = "psci";
 			reg = <4>;
+			operating-points-v2 = <&opp_table4>;
+			opp_table4: opp-table-4 {
+				compatible = "operating-points-v2";
+
+				opp1 { opp-hz = <PLACEHOLDER2>; };
+				opp2 { opp-hz = <PLACEHOLDER2>; };
+				opp3 { opp-hz = <PLACEHOLDER2>; };
+				opp4 { opp-hz = <PLACEHOLDER2>; };
+				opp5 { opp-hz = <PLACEHOLDER2>; };
+				opp6 { opp-hz = <PLACEHOLDER2>; };
+				opp7 { opp-hz = <PLACEHOLDER2>; };
+				opp8 { opp-hz = <PLACEHOLDER2>; };
+				opp9 { opp-hz = <PLACEHOLDER2>; };
+				opp10 { opp-hz = <PLACEHOLDER2>; };
+				opp11 { opp-hz = <PLACEHOLDER2>; };
+				opp12 { opp-hz = <PLACEHOLDER2>; };
+				opp13 { opp-hz = <PLACEHOLDER2>; };
+				opp14 { opp-hz = <PLACEHOLDER2>; };
+				opp15 { opp-hz = <PLACEHOLDER2>; };
+				opp16 { opp-hz = <PLACEHOLDER2>; };
+				opp17 { opp-hz = <PLACEHOLDER2>; };
+				opp18 { opp-hz = <PLACEHOLDER2>; };
+				opp19 { opp-hz = <PLACEHOLDER2>; };
+				opp20 { opp-hz = <PLACEHOLDER2>; };
+			};
 		};
 		cpu@5 {
 			device_type = "cpu";
 			compatible = "arm,arm-v8";
 			enable-method = "psci";
 			reg = <5>;
+			operating-points-v2 = <&opp_table5>;
+			opp_table5: opp-table-5 {
+				compatible = "operating-points-v2";
+
+				opp1 { opp-hz = <PLACEHOLDER2>; };
+				opp2 { opp-hz = <PLACEHOLDER2>; };
+				opp3 { opp-hz = <PLACEHOLDER2>; };
+				opp4 { opp-hz = <PLACEHOLDER2>; };
+				opp5 { opp-hz = <PLACEHOLDER2>; };
+				opp6 { opp-hz = <PLACEHOLDER2>; };
+				opp7 { opp-hz = <PLACEHOLDER2>; };
+				opp8 { opp-hz = <PLACEHOLDER2>; };
+				opp9 { opp-hz = <PLACEHOLDER2>; };
+				opp10 { opp-hz = <PLACEHOLDER2>; };
+				opp11 { opp-hz = <PLACEHOLDER2>; };
+				opp12 { opp-hz = <PLACEHOLDER2>; };
+				opp13 { opp-hz = <PLACEHOLDER2>; };
+				opp14 { opp-hz = <PLACEHOLDER2>; };
+				opp15 { opp-hz = <PLACEHOLDER2>; };
+				opp16 { opp-hz = <PLACEHOLDER2>; };
+				opp17 { opp-hz = <PLACEHOLDER2>; };
+				opp18 { opp-hz = <PLACEHOLDER2>; };
+				opp19 { opp-hz = <PLACEHOLDER2>; };
+				opp20 { opp-hz = <PLACEHOLDER2>; };
+			};
 		};
 		cpu@6 {
 			device_type = "cpu";
 			compatible = "arm,arm-v8";
 			enable-method = "psci";
 			reg = <6>;
+			operating-points-v2 = <&opp_table6>;
+			opp_table6: opp-table-6 {
+				compatible = "operating-points-v2";
+
+				opp1 { opp-hz = <PLACEHOLDER2>; };
+				opp2 { opp-hz = <PLACEHOLDER2>; };
+				opp3 { opp-hz = <PLACEHOLDER2>; };
+				opp4 { opp-hz = <PLACEHOLDER2>; };
+				opp5 { opp-hz = <PLACEHOLDER2>; };
+				opp6 { opp-hz = <PLACEHOLDER2>; };
+				opp7 { opp-hz = <PLACEHOLDER2>; };
+				opp8 { opp-hz = <PLACEHOLDER2>; };
+				opp9 { opp-hz = <PLACEHOLDER2>; };
+				opp10 { opp-hz = <PLACEHOLDER2>; };
+				opp11 { opp-hz = <PLACEHOLDER2>; };
+				opp12 { opp-hz = <PLACEHOLDER2>; };
+				opp13 { opp-hz = <PLACEHOLDER2>; };
+				opp14 { opp-hz = <PLACEHOLDER2>; };
+				opp15 { opp-hz = <PLACEHOLDER2>; };
+				opp16 { opp-hz = <PLACEHOLDER2>; };
+				opp17 { opp-hz = <PLACEHOLDER2>; };
+				opp18 { opp-hz = <PLACEHOLDER2>; };
+				opp19 { opp-hz = <PLACEHOLDER2>; };
+				opp20 { opp-hz = <PLACEHOLDER2>; };
+			};
 		};
 		cpu@7 {
 			device_type = "cpu";
 			compatible = "arm,arm-v8";
 			enable-method = "psci";
 			reg = <7>;
+			operating-points-v2 = <&opp_table7>;
+			opp_table7: opp-table-7 {
+				compatible = "operating-points-v2";
+
+				opp1 { opp-hz = <PLACEHOLDER2>; };
+				opp2 { opp-hz = <PLACEHOLDER2>; };
+				opp3 { opp-hz = <PLACEHOLDER2>; };
+				opp4 { opp-hz = <PLACEHOLDER2>; };
+				opp5 { opp-hz = <PLACEHOLDER2>; };
+				opp6 { opp-hz = <PLACEHOLDER2>; };
+				opp7 { opp-hz = <PLACEHOLDER2>; };
+				opp8 { opp-hz = <PLACEHOLDER2>; };
+				opp9 { opp-hz = <PLACEHOLDER2>; };
+				opp10 { opp-hz = <PLACEHOLDER2>; };
+				opp11 { opp-hz = <PLACEHOLDER2>; };
+				opp12 { opp-hz = <PLACEHOLDER2>; };
+				opp13 { opp-hz = <PLACEHOLDER2>; };
+				opp14 { opp-hz = <PLACEHOLDER2>; };
+				opp15 { opp-hz = <PLACEHOLDER2>; };
+				opp16 { opp-hz = <PLACEHOLDER2>; };
+				opp17 { opp-hz = <PLACEHOLDER2>; };
+				opp18 { opp-hz = <PLACEHOLDER2>; };
+				opp19 { opp-hz = <PLACEHOLDER2>; };
+				opp20 { opp-hz = <PLACEHOLDER2>; };
+			};
 		};
 		cpu@8 {
 			device_type = "cpu";
 			compatible = "arm,arm-v8";
 			enable-method = "psci";
 			reg = <8>;
+
+			operating-points-v2 = <&opp_table8>;
+			opp_table8: opp-table-8 {
+				compatible = "operating-points-v2";
+
+				opp1 { opp-hz = <PLACEHOLDER2>; };
+				opp2 { opp-hz = <PLACEHOLDER2>; };
+				opp3 { opp-hz = <PLACEHOLDER2>; };
+				opp4 { opp-hz = <PLACEHOLDER2>; };
+				opp5 { opp-hz = <PLACEHOLDER2>; };
+				opp6 { opp-hz = <PLACEHOLDER2>; };
+				opp7 { opp-hz = <PLACEHOLDER2>; };
+				opp8 { opp-hz = <PLACEHOLDER2>; };
+				opp9 { opp-hz = <PLACEHOLDER2>; };
+				opp10 { opp-hz = <PLACEHOLDER2>; };
+				opp11 { opp-hz = <PLACEHOLDER2>; };
+				opp12 { opp-hz = <PLACEHOLDER2>; };
+				opp13 { opp-hz = <PLACEHOLDER2>; };
+				opp14 { opp-hz = <PLACEHOLDER2>; };
+				opp15 { opp-hz = <PLACEHOLDER2>; };
+				opp16 { opp-hz = <PLACEHOLDER2>; };
+				opp17 { opp-hz = <PLACEHOLDER2>; };
+				opp18 { opp-hz = <PLACEHOLDER2>; };
+				opp19 { opp-hz = <PLACEHOLDER2>; };
+				opp20 { opp-hz = <PLACEHOLDER2>; };
+			};
 		};
 		cpu@9 {
 			device_type = "cpu";
 			compatible = "arm,arm-v8";
 			enable-method = "psci";
 			reg = <9>;
+			operating-points-v2 = <&opp_table9>;
+			opp_table9: opp-table-9 {
+				compatible = "operating-points-v2";
+
+				opp1 { opp-hz = <PLACEHOLDER2>; };
+				opp2 { opp-hz = <PLACEHOLDER2>; };
+				opp3 { opp-hz = <PLACEHOLDER2>; };
+				opp4 { opp-hz = <PLACEHOLDER2>; };
+				opp5 { opp-hz = <PLACEHOLDER2>; };
+				opp6 { opp-hz = <PLACEHOLDER2>; };
+				opp7 { opp-hz = <PLACEHOLDER2>; };
+				opp8 { opp-hz = <PLACEHOLDER2>; };
+				opp9 { opp-hz = <PLACEHOLDER2>; };
+				opp10 { opp-hz = <PLACEHOLDER2>; };
+				opp11 { opp-hz = <PLACEHOLDER2>; };
+				opp12 { opp-hz = <PLACEHOLDER2>; };
+				opp13 { opp-hz = <PLACEHOLDER2>; };
+				opp14 { opp-hz = <PLACEHOLDER2>; };
+				opp15 { opp-hz = <PLACEHOLDER2>; };
+				opp16 { opp-hz = <PLACEHOLDER2>; };
+				opp17 { opp-hz = <PLACEHOLDER2>; };
+				opp18 { opp-hz = <PLACEHOLDER2>; };
+				opp19 { opp-hz = <PLACEHOLDER2>; };
+				opp20 { opp-hz = <PLACEHOLDER2>; };
+			};
 		};
 		cpu@10 {
 			device_type = "cpu";
 			compatible = "arm,arm-v8";
 			enable-method = "psci";
 			reg = <10>;
+			operating-points-v2 = <&opp_table10>;
+			opp_table10: opp-table-10 {
+				compatible = "operating-points-v2";
+
+				opp1 { opp-hz = <PLACEHOLDER2>; };
+				opp2 { opp-hz = <PLACEHOLDER2>; };
+				opp3 { opp-hz = <PLACEHOLDER2>; };
+				opp4 { opp-hz = <PLACEHOLDER2>; };
+				opp5 { opp-hz = <PLACEHOLDER2>; };
+				opp6 { opp-hz = <PLACEHOLDER2>; };
+				opp7 { opp-hz = <PLACEHOLDER2>; };
+				opp8 { opp-hz = <PLACEHOLDER2>; };
+				opp9 { opp-hz = <PLACEHOLDER2>; };
+				opp10 { opp-hz = <PLACEHOLDER2>; };
+				opp11 { opp-hz = <PLACEHOLDER2>; };
+				opp12 { opp-hz = <PLACEHOLDER2>; };
+				opp13 { opp-hz = <PLACEHOLDER2>; };
+				opp14 { opp-hz = <PLACEHOLDER2>; };
+				opp15 { opp-hz = <PLACEHOLDER2>; };
+				opp16 { opp-hz = <PLACEHOLDER2>; };
+				opp17 { opp-hz = <PLACEHOLDER2>; };
+				opp18 { opp-hz = <PLACEHOLDER2>; };
+				opp19 { opp-hz = <PLACEHOLDER2>; };
+				opp20 { opp-hz = <PLACEHOLDER2>; };
+			};
 		};
 		cpu@11 {
 			device_type = "cpu";
 			compatible = "arm,arm-v8";
 			enable-method = "psci";
 			reg = <11>;
+			operating-points-v2 = <&opp_table11>;
+			opp_table11: opp-table-11 {
+				compatible = "operating-points-v2";
+
+				opp1 { opp-hz = <PLACEHOLDER2>; };
+				opp2 { opp-hz = <PLACEHOLDER2>; };
+				opp3 { opp-hz = <PLACEHOLDER2>; };
+				opp4 { opp-hz = <PLACEHOLDER2>; };
+				opp5 { opp-hz = <PLACEHOLDER2>; };
+				opp6 { opp-hz = <PLACEHOLDER2>; };
+				opp7 { opp-hz = <PLACEHOLDER2>; };
+				opp8 { opp-hz = <PLACEHOLDER2>; };
+				opp9 { opp-hz = <PLACEHOLDER2>; };
+				opp10 { opp-hz = <PLACEHOLDER2>; };
+				opp11 { opp-hz = <PLACEHOLDER2>; };
+				opp12 { opp-hz = <PLACEHOLDER2>; };
+				opp13 { opp-hz = <PLACEHOLDER2>; };
+				opp14 { opp-hz = <PLACEHOLDER2>; };
+				opp15 { opp-hz = <PLACEHOLDER2>; };
+				opp16 { opp-hz = <PLACEHOLDER2>; };
+				opp17 { opp-hz = <PLACEHOLDER2>; };
+				opp18 { opp-hz = <PLACEHOLDER2>; };
+				opp19 { opp-hz = <PLACEHOLDER2>; };
+				opp20 { opp-hz = <PLACEHOLDER2>; };
+			};
 		};
 		cpu@12 {
 			device_type = "cpu";
 			compatible = "arm,arm-v8";
 			enable-method = "psci";
 			reg = <12>;
+			operating-points-v2 = <&opp_table12>;
+			opp_table12: opp-table-12 {
+				compatible = "operating-points-v2";
+
+				opp1 { opp-hz = <PLACEHOLDER2>; };
+				opp2 { opp-hz = <PLACEHOLDER2>; };
+				opp3 { opp-hz = <PLACEHOLDER2>; };
+				opp4 { opp-hz = <PLACEHOLDER2>; };
+				opp5 { opp-hz = <PLACEHOLDER2>; };
+				opp6 { opp-hz = <PLACEHOLDER2>; };
+				opp7 { opp-hz = <PLACEHOLDER2>; };
+				opp8 { opp-hz = <PLACEHOLDER2>; };
+				opp9 { opp-hz = <PLACEHOLDER2>; };
+				opp10 { opp-hz = <PLACEHOLDER2>; };
+				opp11 { opp-hz = <PLACEHOLDER2>; };
+				opp12 { opp-hz = <PLACEHOLDER2>; };
+				opp13 { opp-hz = <PLACEHOLDER2>; };
+				opp14 { opp-hz = <PLACEHOLDER2>; };
+				opp15 { opp-hz = <PLACEHOLDER2>; };
+				opp16 { opp-hz = <PLACEHOLDER2>; };
+				opp17 { opp-hz = <PLACEHOLDER2>; };
+				opp18 { opp-hz = <PLACEHOLDER2>; };
+				opp19 { opp-hz = <PLACEHOLDER2>; };
+				opp20 { opp-hz = <PLACEHOLDER2>; };
+			};
 		};
 		cpu@13 {
 			device_type = "cpu";
 			compatible = "arm,arm-v8";
 			enable-method = "psci";
 			reg = <13>;
+			operating-points-v2 = <&opp_table13>;
+			opp_table13: opp-table-13 {
+				compatible = "operating-points-v2";
+
+				opp1 { opp-hz = <PLACEHOLDER2>; };
+				opp2 { opp-hz = <PLACEHOLDER2>; };
+				opp3 { opp-hz = <PLACEHOLDER2>; };
+				opp4 { opp-hz = <PLACEHOLDER2>; };
+				opp5 { opp-hz = <PLACEHOLDER2>; };
+				opp6 { opp-hz = <PLACEHOLDER2>; };
+				opp7 { opp-hz = <PLACEHOLDER2>; };
+				opp8 { opp-hz = <PLACEHOLDER2>; };
+				opp9 { opp-hz = <PLACEHOLDER2>; };
+				opp10 { opp-hz = <PLACEHOLDER2>; };
+				opp11 { opp-hz = <PLACEHOLDER2>; };
+				opp12 { opp-hz = <PLACEHOLDER2>; };
+				opp13 { opp-hz = <PLACEHOLDER2>; };
+				opp14 { opp-hz = <PLACEHOLDER2>; };
+				opp15 { opp-hz = <PLACEHOLDER2>; };
+				opp16 { opp-hz = <PLACEHOLDER2>; };
+				opp17 { opp-hz = <PLACEHOLDER2>; };
+				opp18 { opp-hz = <PLACEHOLDER2>; };
+				opp19 { opp-hz = <PLACEHOLDER2>; };
+				opp20 { opp-hz = <PLACEHOLDER2>; };
+			};
 		};
 		cpu@14 {
 			device_type = "cpu";
 			compatible = "arm,arm-v8";
 			enable-method = "psci";
 			reg = <14>;
+			operating-points-v2 = <&opp_table14>;
+			opp_table14: opp-table-14 {
+				compatible = "operating-points-v2";
+
+				opp1 { opp-hz = <PLACEHOLDER2>; };
+				opp2 { opp-hz = <PLACEHOLDER2>; };
+				opp3 { opp-hz = <PLACEHOLDER2>; };
+				opp4 { opp-hz = <PLACEHOLDER2>; };
+				opp5 { opp-hz = <PLACEHOLDER2>; };
+				opp6 { opp-hz = <PLACEHOLDER2>; };
+				opp7 { opp-hz = <PLACEHOLDER2>; };
+				opp8 { opp-hz = <PLACEHOLDER2>; };
+				opp9 { opp-hz = <PLACEHOLDER2>; };
+				opp10 { opp-hz = <PLACEHOLDER2>; };
+				opp11 { opp-hz = <PLACEHOLDER2>; };
+				opp12 { opp-hz = <PLACEHOLDER2>; };
+				opp13 { opp-hz = <PLACEHOLDER2>; };
+				opp14 { opp-hz = <PLACEHOLDER2>; };
+				opp15 { opp-hz = <PLACEHOLDER2>; };
+				opp16 { opp-hz = <PLACEHOLDER2>; };
+				opp17 { opp-hz = <PLACEHOLDER2>; };
+				opp18 { opp-hz = <PLACEHOLDER2>; };
+				opp19 { opp-hz = <PLACEHOLDER2>; };
+				opp20 { opp-hz = <PLACEHOLDER2>; };
+			};
 		};
 		cpu@15 {
 			device_type = "cpu";
 			compatible = "arm,arm-v8";
 			enable-method = "psci";
 			reg = <15>;
+			operating-points-v2 = <&opp_table15>;
+			opp_table15: opp-table-15 {
+				compatible = "operating-points-v2";
+
+				opp1 { opp-hz = <PLACEHOLDER2>; };
+				opp2 { opp-hz = <PLACEHOLDER2>; };
+				opp3 { opp-hz = <PLACEHOLDER2>; };
+				opp4 { opp-hz = <PLACEHOLDER2>; };
+				opp5 { opp-hz = <PLACEHOLDER2>; };
+				opp6 { opp-hz = <PLACEHOLDER2>; };
+				opp7 { opp-hz = <PLACEHOLDER2>; };
+				opp8 { opp-hz = <PLACEHOLDER2>; };
+				opp9 { opp-hz = <PLACEHOLDER2>; };
+				opp10 { opp-hz = <PLACEHOLDER2>; };
+				opp11 { opp-hz = <PLACEHOLDER2>; };
+				opp12 { opp-hz = <PLACEHOLDER2>; };
+				opp13 { opp-hz = <PLACEHOLDER2>; };
+				opp14 { opp-hz = <PLACEHOLDER2>; };
+				opp15 { opp-hz = <PLACEHOLDER2>; };
+				opp16 { opp-hz = <PLACEHOLDER2>; };
+				opp17 { opp-hz = <PLACEHOLDER2>; };
+				opp18 { opp-hz = <PLACEHOLDER2>; };
+				opp19 { opp-hz = <PLACEHOLDER2>; };
+				opp20 { opp-hz = <PLACEHOLDER2>; };
+			};
 		};
 	};
 
@@ -321,4 +722,9 @@
 		id = <PLACEHOLDER>;
 		#iommu-cells = <1>;
 	};
+
+	cpufreq {
+		compatible = "virtual,android-v-only-cpufreq";
+		reg = <0x0 0x1040000 PLACEHOLDER2>;
+	};
 };
diff --git a/pvmfw/src/fdt.rs b/pvmfw/src/fdt.rs
index ac52be9..f20451a 100644
--- a/pvmfw/src/fdt.rs
+++ b/pvmfw/src/fdt.rs
@@ -35,6 +35,7 @@
 use libfdt::CellIterator;
 use libfdt::Fdt;
 use libfdt::FdtError;
+use libfdt::FdtNode;
 use libfdt::FdtNodeMut;
 use log::debug;
 use log::error;
@@ -54,12 +55,17 @@
 pub enum FdtValidationError {
     /// Invalid CPU count.
     InvalidCpuCount(usize),
+    /// Invalid VCpufreq Range.
+    InvalidVcpufreq(u64, u64),
 }
 
 impl fmt::Display for FdtValidationError {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         match self {
             Self::InvalidCpuCount(num_cpus) => write!(f, "Invalid CPU count: {num_cpus}"),
+            Self::InvalidVcpufreq(addr, size) => {
+                write!(f, "Invalid vcpufreq region: ({addr:#x}, {size:#x})")
+            }
         }
     }
 }
@@ -172,15 +178,41 @@
         .setprop_inplace(cstr!("reg"), [addr.to_be(), size.to_be()].as_bytes())
 }
 
+//TODO: Need to add info for cpu capacity
 #[derive(Debug, Default)]
-struct CpuInfo {}
+struct CpuInfo {
+    opptable_info: Option<ArrayVec<[u64; CpuInfo::MAX_OPPTABLES]>>,
+}
+
+impl CpuInfo {
+    const MAX_OPPTABLES: usize = 20;
+}
+
+fn read_opp_info_from(
+    opp_node: FdtNode,
+) -> libfdt::Result<ArrayVec<[u64; CpuInfo::MAX_OPPTABLES]>> {
+    let mut table = ArrayVec::new();
+    for subnode in opp_node.subnodes()? {
+        let prop = subnode.getprop_u64(cstr!("opp-hz"))?.ok_or(FdtError::NotFound)?;
+        table.push(prop);
+    }
+
+    Ok(table)
+}
 
 fn read_cpu_info_from(fdt: &Fdt) -> libfdt::Result<ArrayVec<[CpuInfo; DeviceTreeInfo::MAX_CPUS]>> {
     let mut cpus = ArrayVec::new();
-
     let mut cpu_nodes = fdt.compatible_nodes(cstr!("arm,arm-v8"))?;
-    for _cpu in cpu_nodes.by_ref().take(cpus.capacity()) {
-        let info = CpuInfo {};
+    for cpu in cpu_nodes.by_ref().take(cpus.capacity()) {
+        let opp_phandle = cpu.getprop_u32(cstr!("operating-points-v2"))?;
+        let opptable_info = if let Some(phandle) = opp_phandle {
+            let phandle = phandle.try_into()?;
+            let node = fdt.node_with_phandle(phandle)?.ok_or(FdtError::NotFound)?;
+            Some(read_opp_info_from(node)?)
+        } else {
+            None
+        };
+        let info = CpuInfo { opptable_info };
         cpus.push(info);
     }
     if cpu_nodes.next().is_some() {
@@ -198,12 +230,87 @@
     Ok(())
 }
 
+fn read_vcpufreq_info(fdt: &Fdt) -> libfdt::Result<Option<VcpufreqInfo>> {
+    let mut nodes = fdt.compatible_nodes(cstr!("virtual,android-v-only-cpufreq"))?;
+    let Some(node) = nodes.next() else {
+        return Ok(None);
+    };
+
+    if nodes.next().is_some() {
+        warn!("DT has more than 1 cpufreq node: discarding extra nodes.");
+    }
+
+    let mut regs = node.reg()?.ok_or(FdtError::NotFound)?;
+    let reg = regs.next().ok_or(FdtError::NotFound)?;
+    let size = reg.size.ok_or(FdtError::NotFound)?;
+
+    Ok(Some(VcpufreqInfo { addr: reg.addr, size }))
+}
+
+fn validate_vcpufreq_info(
+    vcpufreq_info: &VcpufreqInfo,
+    cpus: &[CpuInfo],
+) -> Result<(), FdtValidationError> {
+    const VCPUFREQ_BASE_ADDR: u64 = 0x1040000;
+    const VCPUFREQ_SIZE_PER_CPU: u64 = 0x8;
+
+    let base = vcpufreq_info.addr;
+    let size = vcpufreq_info.size;
+    let expected_size = VCPUFREQ_SIZE_PER_CPU * cpus.len() as u64;
+
+    if (base, size) != (VCPUFREQ_BASE_ADDR, expected_size) {
+        return Err(FdtValidationError::InvalidVcpufreq(base, size));
+    }
+
+    Ok(())
+}
+
+fn patch_opptable(
+    node: FdtNodeMut,
+    opptable: Option<ArrayVec<[u64; CpuInfo::MAX_OPPTABLES]>>,
+) -> libfdt::Result<()> {
+    let oppcompat = cstr!("operating-points-v2");
+    let next = node.next_compatible(oppcompat)?.ok_or(FdtError::NoSpace)?;
+
+    let Some(opptable) = opptable else {
+        return next.nop();
+    };
+
+    let mut next_subnode = next.first_subnode()?;
+
+    for entry in opptable {
+        let mut subnode = next_subnode.ok_or(FdtError::NoSpace)?;
+        subnode.setprop_inplace(cstr!("opp-hz"), &entry.to_be_bytes())?;
+        next_subnode = subnode.next_subnode()?;
+    }
+
+    while let Some(current) = next_subnode {
+        next_subnode = current.delete_and_next_subnode()?;
+    }
+
+    Ok(())
+}
+
+// TODO(ptosi): Rework FdtNodeMut and replace this function.
+fn get_nth_compatible<'a>(
+    fdt: &'a mut Fdt,
+    n: usize,
+    compat: &CStr,
+) -> libfdt::Result<Option<FdtNodeMut<'a>>> {
+    let mut node = fdt.root_mut()?.next_compatible(compat)?;
+    for _ in 0..n {
+        node = node.ok_or(FdtError::NoSpace)?.next_compatible(compat)?;
+    }
+    Ok(node)
+}
+
 fn patch_cpus(fdt: &mut Fdt, cpus: &[CpuInfo]) -> libfdt::Result<()> {
     const COMPAT: &CStr = cstr!("arm,arm-v8");
-    let mut next = fdt.root_mut()?.next_compatible(COMPAT)?;
-    for _cpu in cpus {
-        next = next.ok_or(FdtError::NoSpace)?.next_compatible(COMPAT)?;
+    for (idx, cpu) in cpus.iter().enumerate() {
+        let cur = get_nth_compatible(fdt, idx, COMPAT)?.ok_or(FdtError::NoSpace)?;
+        patch_opptable(cur, cpu.opptable_info)?;
     }
+    let mut next = get_nth_compatible(fdt, cpus.len(), COMPAT)?;
     while let Some(current) = next {
         next = current.delete_and_next_compatible(COMPAT)?;
     }
@@ -628,6 +735,21 @@
 }
 
 #[derive(Debug)]
+struct VcpufreqInfo {
+    addr: u64,
+    size: u64,
+}
+
+fn patch_vcpufreq(fdt: &mut Fdt, vcpufreq_info: &Option<VcpufreqInfo>) -> libfdt::Result<()> {
+    let mut node = fdt.node_mut(cstr!("/cpufreq"))?.unwrap();
+    if let Some(info) = vcpufreq_info {
+        node.setprop_addrrange_inplace(cstr!("reg"), info.addr, info.size)
+    } else {
+        node.nop()
+    }
+}
+
+#[derive(Debug)]
 pub struct DeviceTreeInfo {
     pub kernel_range: Option<Range<usize>>,
     pub initrd_range: Option<Range<usize>>,
@@ -639,6 +761,7 @@
     pub swiotlb_info: SwiotlbInfo,
     device_assignment: Option<DeviceAssignmentInfo>,
     vm_ref_dt_props_info: BTreeMap<CString, Vec<u8>>,
+    vcpufreq_info: Option<VcpufreqInfo>,
 }
 
 impl DeviceTreeInfo {
@@ -751,6 +874,17 @@
         RebootReason::InvalidFdt
     })?;
 
+    let vcpufreq_info = read_vcpufreq_info(fdt).map_err(|e| {
+        error!("Failed to read vcpufreq info from DT: {e}");
+        RebootReason::InvalidFdt
+    })?;
+    if let Some(ref info) = vcpufreq_info {
+        validate_vcpufreq_info(info, &cpus).map_err(|e| {
+            error!("Failed to validate vcpufreq info from DT: {e}");
+            RebootReason::InvalidFdt
+        })?;
+    }
+
     let pci_info = read_pci_info_from(fdt).map_err(|e| {
         error!("Failed to read pci info from DT: {e}");
         RebootReason::InvalidFdt
@@ -801,6 +935,7 @@
         swiotlb_info,
         device_assignment,
         vm_ref_dt_props_info,
+        vcpufreq_info,
     })
 }
 
@@ -825,6 +960,10 @@
         error!("Failed to patch cpus to DT: {e}");
         RebootReason::InvalidFdt
     })?;
+    patch_vcpufreq(fdt, &info.vcpufreq_info).map_err(|e| {
+        error!("Failed to patch vcpufreq info to DT: {e}");
+        RebootReason::InvalidFdt
+    })?;
     patch_pci_info(fdt, &info.pci_info).map_err(|e| {
         error!("Failed to patch pci info to DT: {e}");
         RebootReason::InvalidFdt
diff --git a/service_vm/test_apk/Android.bp b/service_vm/test_apk/Android.bp
new file mode 100644
index 0000000..8f5fb41
--- /dev/null
+++ b/service_vm/test_apk/Android.bp
@@ -0,0 +1,44 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "VmAttestationTestApp",
+    test_suites: [
+        "general-tests",
+    ],
+    srcs: ["src/java/**/*.java"],
+    static_libs: [
+        "MicrodroidDeviceTestHelper",
+        "androidx.test.runner",
+        "androidx.test.ext.junit",
+        "com.android.virt.vm_attestation.testservice-java",
+        "truth",
+    ],
+    jni_libs: ["libvm_attestation_test_payload"],
+    jni_uses_platform_apis: true,
+    use_embedded_native_libs: true,
+    sdk_version: "test_current",
+    compile_multilib: "first",
+}
+
+rust_defaults {
+    name: "vm_attestation_test_payload_defaults",
+    crate_name: "vm_attestation_test_payload",
+    defaults: ["avf_build_flags_rust"],
+    srcs: ["src/native/main.rs"],
+    prefer_rlib: true,
+    rustlibs: [
+        "com.android.virt.vm_attestation.testservice-rust",
+        "libandroid_logger",
+        "libanyhow",
+        "libavflog",
+        "liblog_rust",
+        "libvm_payload_bindgen",
+    ],
+}
+
+rust_ffi {
+    name: "libvm_attestation_test_payload",
+    defaults: ["vm_attestation_test_payload_defaults"],
+}
diff --git a/service_vm/test_apk/AndroidManifest.xml b/service_vm/test_apk/AndroidManifest.xml
new file mode 100644
index 0000000..b998b7f
--- /dev/null
+++ b/service_vm/test_apk/AndroidManifest.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+      package="com.android.virt.vm_attestation.testapp">
+     <uses-permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE" />
+     <uses-permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE" />
+
+     <application />
+     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.virt.vm_attestation.testapp"
+        android:label="Microdroid VM attestation" />
+</manifest>
diff --git a/service_vm/test_apk/AndroidTest.xml b/service_vm/test_apk/AndroidTest.xml
new file mode 100644
index 0000000..18b4e46
--- /dev/null
+++ b/service_vm/test_apk/AndroidTest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs Microdroid VM remote attestation tests.">
+    <option name="config-descriptor:metadata" key="component" value="security" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+    <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="test-file-name" value="VmAttestationTestApp.apk" />
+    </target_preparer>
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        <option name="run-command" value="mkdir -p /data/local/tmp/cts/microdroid" />
+        <option name="teardown-command" value="rm -rf /data/local/tmp/cts/microdroid" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.virt.vm_attestation.testapp" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+        <option name="shell-timeout" value="300000" />
+        <option name="test-timeout" value="300000" />
+    </test>
+</configuration>
diff --git a/service_vm/test_apk/aidl/Android.bp b/service_vm/test_apk/aidl/Android.bp
new file mode 100644
index 0000000..3ecce46
--- /dev/null
+++ b/service_vm/test_apk/aidl/Android.bp
@@ -0,0 +1,18 @@
+package {
+    default_team: "trendy_team_virtualization",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+aidl_interface {
+    name: "com.android.virt.vm_attestation.testservice",
+    srcs: ["com/android/virt/vm_attestation/testservice/**/*.aidl"],
+    unstable: true,
+    backend: {
+        java: {
+            gen_rpc: true,
+        },
+        rust: {
+            enabled: true,
+        },
+    },
+}
diff --git a/service_vm/test_apk/aidl/com/android/virt/vm_attestation/testservice/IAttestationService.aidl b/service_vm/test_apk/aidl/com/android/virt/vm_attestation/testservice/IAttestationService.aidl
new file mode 100644
index 0000000..94a7b8d
--- /dev/null
+++ b/service_vm/test_apk/aidl/com/android/virt/vm_attestation/testservice/IAttestationService.aidl
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.virt.vm_attestation.testservice;
+
+/** {@hide} */
+interface IAttestationService {
+    const int PORT = 5679;
+
+    /**
+     * Requests attestation for testing.
+     *
+     * A fake key pair should be provisioned with the call to
+     * {@link VirtualMachine#enableTestAttestation()} before calling this method.
+     *
+     * The attestation result will be cached in the VM and can be validated with
+     * {@link #validateAttestationResult}.
+     */
+    void requestAttestationForTesting();
+
+    /**
+     * Validates the attestation result returned by the last call to
+     * {@link #requestAttestationForTesting}.
+     */
+    void validateAttestationResult();
+}
diff --git a/service_vm/test_apk/assets/config.json b/service_vm/test_apk/assets/config.json
new file mode 100644
index 0000000..caae3ce
--- /dev/null
+++ b/service_vm/test_apk/assets/config.json
@@ -0,0 +1,10 @@
+{
+    "os": {
+      "name": "microdroid"
+    },
+    "task": {
+      "type": "microdroid_launcher",
+      "command": "libvm_attestation_test_payload.so"
+    },
+    "export_tombstones": true
+  }
\ No newline at end of file
diff --git a/service_vm/test_apk/src/java/com/android/virt/vm_attestation/testapp/VmAttestationTests.java b/service_vm/test_apk/src/java/com/android/virt/vm_attestation/testapp/VmAttestationTests.java
new file mode 100644
index 0000000..7771e83
--- /dev/null
+++ b/service_vm/test_apk/src/java/com/android/virt/vm_attestation/testapp/VmAttestationTests.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.virt.vm_attestation.testapp;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
+import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_FULL;
+
+import android.system.virtualmachine.VirtualMachine;
+import android.system.virtualmachine.VirtualMachineConfig;
+import android.system.virtualmachine.VirtualMachineManager;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import com.android.microdroid.test.device.MicrodroidDeviceTestBase;
+import com.android.virt.vm_attestation.testservice.IAttestationService;
+
+@RunWith(Parameterized.class)
+public class VmAttestationTests extends MicrodroidDeviceTestBase {
+    private static final String TAG = "VmAttestationTest";
+    private static final String DEFAULT_CONFIG = "assets/config.json";
+
+    @Parameterized.Parameter(0)
+    public String mGki;
+
+    @Parameterized.Parameters(name = "gki={0}")
+    public static Collection<Object[]> params() {
+        List<Object[]> ret = new ArrayList<>();
+        ret.add(new Object[] {null /* use microdroid kernel */});
+        for (String gki : SUPPORTED_GKI_VERSIONS) {
+            ret.add(new Object[] {gki});
+        }
+        return ret;
+    }
+
+    @Before
+    public void setup() throws IOException {
+        grantPermission(VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION);
+        grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
+        prepareTestSetup(true /* protectedVm */, mGki);
+        setMaxPerformanceTaskProfile();
+    }
+
+    @Test
+    public void requestingAttestationSucceeds() throws Exception {
+        assume().withMessage("Remote attestation is not supported on CF.")
+                .that(isCuttlefish())
+                .isFalse();
+        assumeFeatureEnabled(VirtualMachineManager.FEATURE_REMOTE_ATTESTATION);
+
+        VirtualMachineConfig.Builder builder =
+                newVmConfigBuilderWithPayloadConfig(DEFAULT_CONFIG)
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .setVmOutputCaptured(true);
+        VirtualMachineConfig config = builder.build();
+        VirtualMachine vm = forceCreateNewVirtualMachine("attestation_client", config);
+
+        vm.enableTestAttestation();
+        CompletableFuture<Exception> exception = new CompletableFuture<>();
+        CompletableFuture<Boolean> payloadReady = new CompletableFuture<>();
+        VmEventListener listener =
+                new VmEventListener() {
+                    @Override
+                    public void onPayloadReady(VirtualMachine vm) {
+                        payloadReady.complete(true);
+                        try {
+                            IAttestationService service =
+                                    IAttestationService.Stub.asInterface(
+                                            vm.connectToVsockServer(IAttestationService.PORT));
+                            android.os.Trace.beginSection("runningVmRequestsAttestation");
+                            service.requestAttestationForTesting();
+                            android.os.Trace.endSection();
+                            service.validateAttestationResult();
+                        } catch (Exception e) {
+                            exception.complete(e);
+                        } finally {
+                            forceStop(vm);
+                        }
+                    }
+                };
+
+        listener.runToFinish(TAG, vm);
+        assertThat(payloadReady.getNow(false)).isTrue();
+        assertThat(exception.getNow(null)).isNull();
+    }
+}
diff --git a/service_vm/test_apk/src/native/main.rs b/service_vm/test_apk/src/native/main.rs
new file mode 100644
index 0000000..d5d599d
--- /dev/null
+++ b/service_vm/test_apk/src/native/main.rs
@@ -0,0 +1,271 @@
+// Copyright 2024, The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Main executable of VM attestation for end-to-end testing.
+
+use anyhow::{anyhow, ensure, Result};
+use avflog::LogResult;
+use com_android_virt_vm_attestation_testservice::{
+    aidl::com::android::virt::vm_attestation::testservice::IAttestationService::{
+        BnAttestationService, IAttestationService, PORT,
+    },
+    binder::{self, unstable_api::AsNative, BinderFeatures, Interface, IntoBinderResult, Strong},
+};
+use log::{error, info};
+use std::{
+    ffi::{c_void, CStr},
+    panic,
+    ptr::{self, NonNull},
+    result,
+    sync::{Arc, Mutex},
+};
+use vm_payload_bindgen::{
+    attestation_status_t, AIBinder, AVmAttestationResult, AVmAttestationResult_free,
+    AVmAttestationResult_getCertificateAt, AVmAttestationResult_getCertificateCount,
+    AVmAttestationResult_getPrivateKey, AVmAttestationResult_resultToString,
+    AVmAttestationResult_sign, AVmPayload_notifyPayloadReady,
+    AVmPayload_requestAttestationForTesting, AVmPayload_runVsockRpcServer,
+};
+
+/// Entry point of the Service VM client.
+#[allow(non_snake_case)]
+#[no_mangle]
+pub extern "C" fn AVmPayload_main() {
+    android_logger::init_once(
+        android_logger::Config::default()
+            .with_tag("service_vm_client")
+            .with_max_level(log::LevelFilter::Debug),
+    );
+    // Redirect panic messages to logcat.
+    panic::set_hook(Box::new(|panic_info| {
+        error!("{}", panic_info);
+    }));
+    if let Err(e) = try_main() {
+        error!("failed with {:?}", e);
+        std::process::exit(1);
+    }
+}
+
+fn try_main() -> Result<()> {
+    info!("Welcome to Service VM Client!");
+
+    let mut service = AttestationService::new_binder().as_binder();
+    let service = service.as_native_mut() as *mut AIBinder;
+    let param = ptr::null_mut();
+    // SAFETY: We hold a strong pointer, so the raw pointer remains valid. The bindgen AIBinder
+    // is the same type as `sys::AIBinder`. It is safe for `on_ready` to be invoked at any time,
+    // with any parameter.
+    unsafe { AVmPayload_runVsockRpcServer(service, PORT.try_into()?, Some(on_ready), param) };
+}
+
+extern "C" fn on_ready(_param: *mut c_void) {
+    // SAFETY: It is safe to call `AVmPayload_notifyPayloadReady` at any time.
+    unsafe { AVmPayload_notifyPayloadReady() };
+}
+
+struct AttestationService {
+    res: Arc<Mutex<Option<AttestationResult>>>,
+}
+
+impl Interface for AttestationService {}
+
+impl AttestationService {
+    fn new_binder() -> Strong<dyn IAttestationService> {
+        let res = Arc::new(Mutex::new(None));
+        BnAttestationService::new_binder(AttestationService { res }, BinderFeatures::default())
+    }
+}
+
+impl IAttestationService for AttestationService {
+    fn requestAttestationForTesting(&self) -> binder::Result<()> {
+        // 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,
+        ];
+        let res = AttestationResult::request_attestation(challenge)
+            .map_err(|e| anyhow!("Unexpected status: {:?}", status_to_cstr(e)))
+            .with_log()
+            .or_service_specific_exception(-1)?;
+        *self.res.lock().unwrap() = Some(res);
+        Ok(())
+    }
+
+    fn validateAttestationResult(&self) -> binder::Result<()> {
+        // TODO(b/191073073): Returns the attestation result to the host for validation.
+        self.res.lock().unwrap().as_ref().unwrap().log().or_service_specific_exception(-1)
+    }
+}
+
+#[derive(Debug)]
+struct AttestationResult(NonNull<AVmAttestationResult>);
+
+// Safety: `AttestationResult` is not `Send` because it contains a raw pointer to a C struct.
+unsafe impl Send for AttestationResult {}
+
+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_requestAttestationForTesting(
+                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)
+    }
+
+    fn log(&self) -> Result<()> {
+        let cert_chain = self.certificate_chain()?;
+        info!("Attestation result certificateChain = {:?}", cert_chain);
+
+        let private_key = self.private_key()?;
+        info!("Attestation result privateKey = {:?}", private_key);
+
+        let message = b"Hello from Service VM client";
+        info!("Signing message: {:?}", message);
+        let signature = self.sign(message)?;
+        info!("Signature: {:?}", signature);
+        Ok(())
+    }
+}
+
+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 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(),
+        )
+    };
+    ensure!(size <= signature.len());
+    signature.truncate(size);
+    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 that lives forever.
+    unsafe { CStr::from_ptr(message) }
+}
diff --git a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
index e9c84fb..2d52732 100644
--- a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
+++ b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
@@ -49,7 +49,6 @@
 
 import org.junit.After;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.Timeout;
@@ -277,10 +276,6 @@
                 (builder) -> builder);
     }
 
-    // TODO(b/323768068): Enable this test when we can inject vendor digest for test purpose.
-    // After introducing VM reference DT, non-pVM cannot trust test_microdroid_vendor_image.img
-    // as well, because it doesn't pass the hashtree digest of testing image into VM.
-    @Ignore
     @Test
     public void testMicrodroidDebugBootTime_withVendorPartition() throws Exception {
         assume().withMessage("Cuttlefish doesn't support device tree under" + " /proc/device-tree")
@@ -293,8 +288,10 @@
                 .isFalse();
         assumeFeatureEnabled(VirtualMachineManager.FEATURE_VENDOR_MODULES);
 
-        File vendorDiskImage =
-                new File("/data/local/tmp/microdroid-bench/microdroid_vendor_image.img");
+        File vendorDiskImage = new File("/vendor/etc/avf/microdroid/microdroid_vendor.img");
+        assume().withMessage("Microdroid vendor image doesn't exist, skip")
+                .that(vendorDiskImage.exists())
+                .isTrue();
         runBootTimeTest(
                 "test_vm_boot_time_debug_with_vendor_partition",
                 "assets/" + os() + "/vm_config.json",
diff --git a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
index 4e340f0..0687a7b 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -73,7 +73,6 @@
 
 import org.junit.After;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.function.ThrowingRunnable;
@@ -2157,10 +2156,6 @@
                 .contains("android.permission.USE_CUSTOM_VIRTUAL_MACHINE permission");
     }
 
-    // TODO(b/323768068): Enable this test when we can inject vendor digest for test purpose.
-    // After introducing VM reference DT, non-pVM cannot trust test_microdroid_vendor_image.img
-    // as well, because it doesn't pass the hashtree digest of testing image into VM.
-    @Ignore
     @Test
     public void bootsWithVendorPartition() throws Exception {
         assumeSupportedDevice();
@@ -2174,8 +2169,8 @@
 
         grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
 
-        File vendorDiskImage =
-                new File("/data/local/tmp/cts/microdroid/test_microdroid_vendor_image.img");
+        File vendorDiskImage = new File("/vendor/etc/avf/microdroid/microdroid_vendor.img");
+        assumeTrue("Microdroid vendor image doesn't exist, skip", vendorDiskImage.exists());
         VirtualMachineConfig config =
                 newVmConfigBuilderWithPayloadBinary("MicrodroidTestNativeLib.so")
                         .setVendorDiskImage(vendorDiskImage)
diff --git a/virtualizationmanager/src/aidl.rs b/virtualizationmanager/src/aidl.rs
index 6a8f1a1..a2194cc 100644
--- a/virtualizationmanager/src/aidl.rs
+++ b/virtualizationmanager/src/aidl.rs
@@ -38,6 +38,7 @@
     IVirtualizationService::FEATURE_MULTI_TENANT,
     IVirtualizationService::FEATURE_VENDOR_MODULES,
     IVirtualizationService::FEATURE_DICE_CHANGES,
+    IVirtualizationService::FEATURE_REMOTE_ATTESTATION,
     MemoryTrimLevel::MemoryTrimLevel,
     Partition::Partition,
     PartitionType::PartitionType,
@@ -311,6 +312,7 @@
         match feature {
             FEATURE_DICE_CHANGES => Ok(cfg!(dice_changes)),
             FEATURE_MULTI_TENANT => Ok(cfg!(multi_tenant)),
+            FEATURE_REMOTE_ATTESTATION => Ok(cfg!(remote_attestation)),
             FEATURE_VENDOR_MODULES => Ok(cfg!(vendor_modules)),
             _ => {
                 warn!("unknown feature {feature}");
@@ -318,6 +320,10 @@
             }
         }
     }
+
+    fn enableTestAttestation(&self) -> binder::Result<()> {
+        GLOBAL_SERVICE.enableTestAttestation()
+    }
 }
 
 impl VirtualizationService {
@@ -1419,8 +1425,8 @@
         Ok(sk.map(|s| BnSecretkeeper::new_binder(SecretkeeperProxy(s), BinderFeatures::default())))
     }
 
-    fn requestAttestation(&self, csr: &[u8]) -> binder::Result<Vec<Certificate>> {
-        GLOBAL_SERVICE.requestAttestation(csr, get_calling_uid() as i32)
+    fn requestAttestation(&self, csr: &[u8], test_mode: bool) -> binder::Result<Vec<Certificate>> {
+        GLOBAL_SERVICE.requestAttestation(csr, get_calling_uid() as i32, test_mode)
     }
 }
 
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
index 92a5812..7962bc3 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
@@ -24,6 +24,7 @@
 interface IVirtualizationService {
     const String FEATURE_DICE_CHANGES = "com.android.kvm.DICE_CHANGES";
     const String FEATURE_MULTI_TENANT = "com.android.kvm.MULTI_TENANT";
+    const String FEATURE_REMOTE_ATTESTATION = "com.android.kvm.REMOTE_ATTESTATION";
     const String FEATURE_VENDOR_MODULES = "com.android.kvm.VENDOR_MODULES";
 
     /**
@@ -73,4 +74,10 @@
 
     /** Returns whether given feature is enabled. */
     boolean isFeatureEnabled(in String feature);
+
+    /**
+     * Provisions a key pair for the VM attestation testing, a fake certificate will be
+     * associated to the fake key pair when the VM requests attestation in testing mode.
+     */
+    void enableTestAttestation();
 }
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
index dd94526..abfc45a 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
@@ -61,10 +61,20 @@
      *                     attested is owned by this app.
      *                     The uniqueness of the UID ensures that no two VMs owned by different apps
      *                     are able to correlate keys.
+     * @param testMode Whether the request is for testing purposes.
      * @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.
      */
-    Certificate[] requestAttestation(in byte[] csr, int requesterUid);
+    Certificate[] requestAttestation(in byte[] csr, int requesterUid, in boolean testMode);
+
+    /**
+     * Provisions a key pair for the VM attestation testing, a fake certificate will be
+     * associated to the fake key pair when the VM requests attestation in testing mode.
+     *
+     * The provisioned key pair will be used in the subsequent call to {@link #requestAttestation}
+     * with testMode set to true.
+     */
+    void enableTestAttestation();
 
     /**
      * 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 cf91302..6806a5c 100644
--- a/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl
+++ b/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl
@@ -51,10 +51,11 @@
      * Requests a certificate chain for the provided certificate signing request (CSR).
      *
      * @param csr The certificate signing request.
+     * @param testMode Whether the request is for test purposes.
      * @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.
      */
-    Certificate[] requestAttestation(in byte[] csr);
+    Certificate[] requestAttestation(in byte[] csr, in boolean testMode);
 
     /**
      * Request connection to Secretkeeper. This is used by pVM to store Anti-Rollback protected
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index a1a1fb9..d0c5d4a 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -16,7 +16,8 @@
 
 use crate::{get_calling_pid, get_calling_uid, REMOTELY_PROVISIONED_COMPONENT_SERVICE_NAME};
 use crate::atom::{forward_vm_booted_atom, forward_vm_creation_atom, forward_vm_exited_atom};
-use crate::rkpvm::request_attestation;
+use crate::rkpvm::{request_attestation, generate_ecdsa_p256_key_pair};
+use crate::remote_provisioning;
 use android_os_permissions_aidl::aidl::android::os::IPermissionController;
 use android_system_virtualizationcommon::aidl::android::system::virtualizationcommon::Certificate::Certificate;
 use android_system_virtualizationservice::{
@@ -38,6 +39,7 @@
 use anyhow::{anyhow, ensure, Context, Result};
 use avflog::LogResult;
 use binder::{self, wait_for_interface, BinderFeatures, ExceptionCode, Interface, LazyServiceGuard, Status, Strong, IntoBinderResult};
+use service_vm_comm::Response;
 use lazy_static::lazy_static;
 use libc::VMADDR_CID_HOST;
 use log::{error, info, warn};
@@ -73,7 +75,73 @@
 
 const CHUNK_RECV_MAX_LEN: usize = 1024;
 
+/// The fake certificate is used for testing only when a client VM requests attestation in test
+/// mode, it is a single certificate extracted on an unregistered device for testing.
+/// Here is the snapshot of the certificate:
+///
+/// ```
+/// Certificate:
+/// Data:
+/// Version: 3 (0x2)
+/// Serial Number:
+///     59:ae:50:98:95:e1:34:25:f1:21:93:c0:4c:e5:24:66
+/// Signature Algorithm: ecdsa-with-SHA256
+/// Issuer: CN = Droid Unregistered Device CA, O = Google Test LLC
+/// Validity
+///     Not Before: Feb  5 14:39:39 2024 GMT
+///     Not After : Feb 14 14:39:39 2024 GMT
+/// Subject: CN = 59ae509895e13425f12193c04ce52466, O = TEE
+/// Subject Public Key Info:
+///     Public Key Algorithm: id-ecPublicKey
+///         Public-Key: (256 bit)
+///         pub:
+///             04:30:32:cd:95:12:b0:71:8b:b7:14:44:26:58:d5:
+///             82:8c:25:55:2c:6d:ef:98:e3:4f:88:d0:74:82:09:
+///             3e:8d:6c:f0:f2:18:d5:83:0e:0d:f2:ce:c5:15:38:
+///             e5:6a:e6:4d:4d:95:15:b7:24:e7:cb:4b:63:42:21:
+///             bc:36:c6:0a:d8
+///         ASN1 OID: prime256v1
+///         NIST CURVE: P-256
+/// X509v3 extensions:
+///  ...
+/// ```
+const FAKE_CERTIFICATE_FOR_TESTING: &[u8] = &[
+    0x30, 0x82, 0x01, 0xee, 0x30, 0x82, 0x01, 0x94, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x10, 0x59,
+    0xae, 0x50, 0x98, 0x95, 0xe1, 0x34, 0x25, 0xf1, 0x21, 0x93, 0xc0, 0x4c, 0xe5, 0x24, 0x66, 0x30,
+    0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x30, 0x41, 0x31, 0x25, 0x30,
+    0x23, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x1c, 0x44, 0x72, 0x6f, 0x69, 0x64, 0x20, 0x55, 0x6e,
+    0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x20, 0x44, 0x65, 0x76, 0x69, 0x63,
+    0x65, 0x20, 0x43, 0x41, 0x31, 0x18, 0x30, 0x16, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x0f, 0x47,
+    0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x20, 0x54, 0x65, 0x73, 0x74, 0x20, 0x4c, 0x4c, 0x43, 0x30, 0x1e,
+    0x17, 0x0d, 0x32, 0x34, 0x30, 0x32, 0x30, 0x35, 0x31, 0x34, 0x33, 0x39, 0x33, 0x39, 0x5a, 0x17,
+    0x0d, 0x32, 0x34, 0x30, 0x32, 0x31, 0x34, 0x31, 0x34, 0x33, 0x39, 0x33, 0x39, 0x5a, 0x30, 0x39,
+    0x31, 0x29, 0x30, 0x27, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x20, 0x35, 0x39, 0x61, 0x65, 0x35,
+    0x30, 0x39, 0x38, 0x39, 0x35, 0x65, 0x31, 0x33, 0x34, 0x32, 0x35, 0x66, 0x31, 0x32, 0x31, 0x39,
+    0x33, 0x63, 0x30, 0x34, 0x63, 0x65, 0x35, 0x32, 0x34, 0x36, 0x36, 0x31, 0x0c, 0x30, 0x0a, 0x06,
+    0x03, 0x55, 0x04, 0x0a, 0x13, 0x03, 0x54, 0x45, 0x45, 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a,
+    0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07,
+    0x03, 0x42, 0x00, 0x04, 0x30, 0x32, 0xcd, 0x95, 0x12, 0xb0, 0x71, 0x8b, 0xb7, 0x14, 0x44, 0x26,
+    0x58, 0xd5, 0x82, 0x8c, 0x25, 0x55, 0x2c, 0x6d, 0xef, 0x98, 0xe3, 0x4f, 0x88, 0xd0, 0x74, 0x82,
+    0x09, 0x3e, 0x8d, 0x6c, 0xf0, 0xf2, 0x18, 0xd5, 0x83, 0x0e, 0x0d, 0xf2, 0xce, 0xc5, 0x15, 0x38,
+    0xe5, 0x6a, 0xe6, 0x4d, 0x4d, 0x95, 0x15, 0xb7, 0x24, 0xe7, 0xcb, 0x4b, 0x63, 0x42, 0x21, 0xbc,
+    0x36, 0xc6, 0x0a, 0xd8, 0xa3, 0x76, 0x30, 0x74, 0x30, 0x1d, 0x06, 0x03, 0x55, 0x1d, 0x0e, 0x04,
+    0x16, 0x04, 0x14, 0x39, 0x81, 0x41, 0x0a, 0xb9, 0xf3, 0xf4, 0x5b, 0x75, 0x97, 0x4a, 0x46, 0xd6,
+    0x30, 0x9e, 0x1d, 0x7a, 0x3b, 0xec, 0xa8, 0x30, 0x1f, 0x06, 0x03, 0x55, 0x1d, 0x23, 0x04, 0x18,
+    0x30, 0x16, 0x80, 0x14, 0x82, 0xbd, 0x00, 0xde, 0xcb, 0xc5, 0xe7, 0x72, 0x87, 0x3d, 0x1c, 0x0a,
+    0x1e, 0x78, 0x4f, 0xf5, 0xd3, 0xc1, 0x3e, 0xb8, 0x30, 0x0f, 0x06, 0x03, 0x55, 0x1d, 0x13, 0x01,
+    0x01, 0xff, 0x04, 0x05, 0x30, 0x03, 0x01, 0x01, 0xff, 0x30, 0x0e, 0x06, 0x03, 0x55, 0x1d, 0x0f,
+    0x01, 0x01, 0xff, 0x04, 0x04, 0x03, 0x02, 0x02, 0x04, 0x30, 0x11, 0x06, 0x0a, 0x2b, 0x06, 0x01,
+    0x04, 0x01, 0xd6, 0x79, 0x02, 0x01, 0x1e, 0x04, 0x03, 0xa1, 0x01, 0x08, 0x30, 0x0a, 0x06, 0x08,
+    0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x03, 0x48, 0x00, 0x30, 0x45, 0x02, 0x21, 0x00,
+    0xae, 0xd8, 0x40, 0x9e, 0x37, 0x3e, 0x5c, 0x9c, 0xe2, 0x93, 0x3d, 0x8c, 0xf7, 0x05, 0x10, 0xe7,
+    0xd1, 0x2b, 0x87, 0x8a, 0xee, 0xd6, 0x1e, 0x6c, 0x3b, 0xd2, 0x91, 0x3e, 0xa5, 0xdf, 0x91, 0x20,
+    0x02, 0x20, 0x7f, 0x0f, 0x29, 0x54, 0x60, 0x80, 0x07, 0x50, 0x5f, 0x56, 0x6b, 0x9f, 0xe0, 0x94,
+    0xb4, 0x3f, 0x3b, 0x0f, 0x61, 0xa0, 0x33, 0x40, 0xe6, 0x1a, 0x42, 0xda, 0x4b, 0xa4, 0xfd, 0x92,
+    0xb9, 0x0f,
+];
+
 lazy_static! {
+    static ref FAKE_PROVISIONED_KEY_BLOB_FOR_TESTING: Mutex<Option<Vec<u8>>> = Mutex::new(None);
     static ref VFIO_SERVICE: Strong<dyn IVfioHandler> =
         wait_for_interface(<BpVfioHandler as IVfioHandler>::get_descriptor())
             .expect("Could not connect to VfioHandler");
@@ -169,10 +237,41 @@
         Ok(cids)
     }
 
+    fn enableTestAttestation(&self) -> binder::Result<()> {
+        check_manage_access()?;
+        check_use_custom_virtual_machine()?;
+        if !cfg!(remote_attestation) {
+            return Err(Status::new_exception_str(
+                ExceptionCode::UNSUPPORTED_OPERATION,
+                Some(
+                    "enableTestAttestation is not supported with the remote_attestation \
+                     feature disabled",
+                ),
+            ))
+            .with_log();
+        }
+        let res = generate_ecdsa_p256_key_pair()
+            .context("Failed to generate ECDSA P-256 key pair for testing")
+            .with_log()
+            .or_service_specific_exception(-1)?;
+        match res {
+            Response::GenerateEcdsaP256KeyPair(key_pair) => {
+                FAKE_PROVISIONED_KEY_BLOB_FOR_TESTING
+                    .lock()
+                    .unwrap()
+                    .replace(key_pair.key_blob.to_vec());
+                Ok(())
+            }
+            _ => Err(remote_provisioning::to_service_specific_error(res)),
+        }
+        .with_log()
+    }
+
     fn requestAttestation(
         &self,
         csr: &[u8],
         requester_uid: i32,
+        test_mode: bool,
     ) -> binder::Result<Vec<Certificate>> {
         check_manage_access()?;
         if !cfg!(remote_attestation) {
@@ -186,14 +285,31 @@
             .with_log();
         }
         info!("Received csr. Requestting attestation...");
-        let attestation_key = get_rkpd_attestation_key(
-            REMOTELY_PROVISIONED_COMPONENT_SERVICE_NAME,
-            requester_uid as u32,
-        )
-        .context("Failed to retrieve the remotely provisioned keys")
-        .with_log()
-        .or_service_specific_exception(-1)?;
-        let mut certificate_chain = split_x509_certificate_chain(&attestation_key.encodedCertChain)
+        let (key_blob, certificate_chain) = if test_mode {
+            check_use_custom_virtual_machine()?;
+            info!("Using the fake key blob for testing...");
+            (
+                FAKE_PROVISIONED_KEY_BLOB_FOR_TESTING
+                    .lock()
+                    .unwrap()
+                    .clone()
+                    .ok_or_else(|| anyhow!("No key blob for testing"))
+                    .with_log()
+                    .or_service_specific_exception(-1)?,
+                FAKE_CERTIFICATE_FOR_TESTING.to_vec(),
+            )
+        } else {
+            info!("Retrieving the remotely provisioned keys from RKPD...");
+            let attestation_key = get_rkpd_attestation_key(
+                REMOTELY_PROVISIONED_COMPONENT_SERVICE_NAME,
+                requester_uid as u32,
+            )
+            .context("Failed to retrieve the remotely provisioned keys")
+            .with_log()
+            .or_service_specific_exception(-1)?;
+            (attestation_key.keyBlob, attestation_key.encodedCertChain)
+        };
+        let mut certificate_chain = split_x509_certificate_chain(&certificate_chain)
             .context("Failed to split the remotely provisioned certificate chain")
             .with_log()
             .or_service_specific_exception(-1)?;
@@ -206,7 +322,7 @@
         }
         let certificate = request_attestation(
             csr.to_vec(),
-            attestation_key.keyBlob,
+            key_blob,
             certificate_chain[0].encodedCertificate.clone(),
         )
         .context("Failed to request attestation")
diff --git a/virtualizationservice/src/remote_provisioning.rs b/virtualizationservice/src/remote_provisioning.rs
index 40f54db..c2c04df 100644
--- a/virtualizationservice/src/remote_provisioning.rs
+++ b/virtualizationservice/src/remote_provisioning.rs
@@ -145,7 +145,7 @@
     }
 }
 
-fn to_service_specific_error(response: Response) -> Status {
+pub(crate) fn to_service_specific_error(response: Response) -> Status {
     match response {
         Response::Err(e) => match e {
             RequestProcessingError::InvalidMac => {
diff --git a/vm_payload/include-restricted/vm_payload_restricted.h b/vm_payload/include-restricted/vm_payload_restricted.h
index 15c37ed..d7324a8 100644
--- a/vm_payload/include-restricted/vm_payload_restricted.h
+++ b/vm_payload/include-restricted/vm_payload_restricted.h
@@ -55,4 +55,25 @@
  */
 size_t AVmPayload_getDiceAttestationCdi(void* _Nullable data, size_t size);
 
+/**
+ * Requests attestation for the VM for testing only.
+ *
+ * This function is only for testing and will not return a real RKP server backed
+ * certificate chain.
+ *
+ * Prior to calling this function, the caller must provision a key pair to be used in
+ * this function with `VirtualMachineManager#enableTestAttestation`.
+ *
+ * \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`.
+ */
+attestation_status_t AVmPayload_requestAttestationForTesting(
+        const void* _Nonnull challenge, size_t challenge_size,
+        struct AVmAttestationResult* _Nullable* _Nonnull result) __INTRODUCED_IN(__ANDROID_API_V__);
+
 __END_DECLS
diff --git a/vm_payload/include/vm_payload.h b/vm_payload/include/vm_payload.h
index 3483e1d..af755c9 100644
--- a/vm_payload/include/vm_payload.h
+++ b/vm_payload/include/vm_payload.h
@@ -211,7 +211,7 @@
  * 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.
+ * \return The size of the signature, or the size needed if the supplied buffer is too small.
  *
  * [RFC 6979]: https://datatracker.ietf.org/doc/html/rfc6979
  */
diff --git a/vm_payload/libvm_payload.map.txt b/vm_payload/libvm_payload.map.txt
index 975a5a3..caf8f84 100644
--- a/vm_payload/libvm_payload.map.txt
+++ b/vm_payload/libvm_payload.map.txt
@@ -8,6 +8,7 @@
     AVmPayload_getApkContentsPath;       # systemapi introduced=UpsideDownCake
     AVmPayload_getEncryptedStoragePath;  # systemapi introduced=UpsideDownCake
     AVmPayload_requestAttestation;       # systemapi introduced=VanillaIceCream
+    AVmPayload_requestAttestationForTesting; # systemapi introduced=VanillaIceCream
     AVmAttestationResult_getPrivateKey;  # systemapi introduced=VanillaIceCream
     AVmAttestationResult_sign;           # systemapi introduced=VanillaIceCream
     AVmAttestationResult_free;           # systemapi introduced=VanillaIceCream
diff --git a/vm_payload/src/lib.rs b/vm_payload/src/lib.rs
index 7978059..6188b21 100644
--- a/vm_payload/src/lib.rs
+++ b/vm_payload/src/lib.rs
@@ -39,6 +39,9 @@
 };
 use vm_payload_status_bindgen::attestation_status_t;
 
+/// Maximum size of an ECDSA signature for EC P-256 key is 72 bytes.
+const MAX_ECDSA_P256_SIGNATURE_SIZE: usize = 72;
+
 lazy_static! {
     static ref VM_APK_CONTENTS_PATH_C: CString =
         CString::new(VM_APK_CONTENTS_PATH).expect("CString::new failed");
@@ -273,9 +276,6 @@
 /// Behavior is undefined if any of the following conditions are violated:
 ///
 /// * `challenge` must be [valid] for reads of `challenge_size` bytes.
-/// * `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]
@@ -284,6 +284,60 @@
     challenge_size: usize,
     res: &mut *mut AttestationResult,
 ) -> attestation_status_t {
+    // SAFETY: The caller guarantees that `challenge` is valid for reads and `res` is valid
+    // for writes.
+    unsafe {
+        request_attestation(
+            challenge,
+            challenge_size,
+            false, // test_mode
+            res,
+        )
+    }
+}
+
+/// Requests the remote attestation of the client VM for testing.
+///
+/// # Safety
+///
+/// Behavior is undefined if any of the following conditions are violated:
+///
+/// * `challenge` must be [valid] for reads of `challenge_size` bytes.
+///
+/// [valid]: ptr#safety
+#[no_mangle]
+pub unsafe extern "C" fn AVmPayload_requestAttestationForTesting(
+    challenge: *const u8,
+    challenge_size: usize,
+    res: &mut *mut AttestationResult,
+) -> attestation_status_t {
+    // SAFETY: The caller guarantees that `challenge` is valid for reads and `res` is valid
+    // for writes.
+    unsafe {
+        request_attestation(
+            challenge,
+            challenge_size,
+            true, // test_mode
+            res,
+        )
+    }
+}
+
+/// Requests the remote attestation of the client VM.
+///
+/// # Safety
+///
+/// Behavior is undefined if any of the following conditions are violated:
+///
+/// * `challenge` must be [valid] for reads of `challenge_size` bytes.
+///
+/// [valid]: ptr#safety
+unsafe fn request_attestation(
+    challenge: *const u8,
+    challenge_size: usize,
+    test_mode: bool,
+    res: &mut *mut AttestationResult,
+) -> attestation_status_t {
     initialize_logging();
     const MAX_CHALLENGE_SIZE: usize = 64;
     if challenge_size > MAX_CHALLENGE_SIZE {
@@ -297,7 +351,7 @@
         unsafe { std::slice::from_raw_parts(challenge, challenge_size) }
     };
     let service = unwrap_or_abort(get_vm_payload_service());
-    match service.requestAttestation(challenge) {
+    match service.requestAttestation(challenge, test_mode) {
         Ok(attestation_res) => {
             *res = Box::into_raw(Box::new(attestation_res));
             attestation_status_t::ATTESTATION_OK
@@ -400,27 +454,36 @@
     data: *mut u8,
     size: usize,
 ) -> usize {
+    // A DER-encoded ECDSA signature can have varying sizes even with the same EC Key and message,
+    // due to the encoding of the random values r and s that are part of the signature.
+    if size == 0 {
+        return MAX_ECDSA_P256_SIGNATURE_SIZE;
+    }
     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),
-            )
-        };
+    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(),
+            usize::min(signature.len(), size),
+        )
+    };
+    if size < signature.len() {
+        // If the buffer is too small, return the maximum size of the signature to allow the caller
+        // to allocate a buffer large enough to call this function again.
+        MAX_ECDSA_P256_SIGNATURE_SIZE
+    } else {
+        signature.len()
     }
-    signature.len()
 }
 
 fn try_ecdsa_sign(message: &[u8], der_encoded_ec_private_key: &[u8]) -> Result<Vec<u8>> {