Merge "microdroid: Sign images using pvmfw_embedded_key" into main
diff --git a/OWNERS b/OWNERS
index e560cec..40c709f 100644
--- a/OWNERS
+++ b/OWNERS
@@ -4,7 +4,6 @@
 #
 # If you are not a member of the project please send review requests
 # to one of those listed below.
-dbrazdil@google.com
 jiyong@google.com
 smoreland@google.com
 willdeacon@google.com
@@ -13,6 +12,7 @@
 alanstokes@google.com
 aliceywang@google.com
 inseob@google.com
+ioffe@google.com
 jaewan@google.com
 jakobvukalovic@google.com
 jeffv@google.com
diff --git a/TEST_MAPPING b/TEST_MAPPING
index db0b43a..3651dfa 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -66,6 +66,11 @@
       "keywords": ["internal"]
     }
   ],
+  "ferrochrome-postsubmit": [
+    {
+      "name": "ferrochrome-tests"
+    }
+  ],
   "postsubmit": [
     {
       "name": "CtsMicrodroidDisabledTestCases"
diff --git a/apex/Android.bp b/apex/Android.bp
index 99b2dee..17b1f9e 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -47,7 +47,7 @@
         "release_avf_enable_device_assignment",
         "release_avf_enable_llpvm_changes",
         "release_avf_enable_network",
-        "release_avf_enable_remote_attestation",
+        "avf_remote_attestation_enabled",
         "release_avf_enable_vendor_modules",
         "release_avf_enable_virt_cpufreq",
         "release_avf_support_custom_vm_with_paravirtualized_devices",
@@ -157,6 +157,7 @@
         "microdroid.json",
         "microdroid_kernel",
         "com.android.virt.init.rc",
+        "android_bootloader_crosvm_aarch64",
     ],
     host_required: [
         "vm_shell",
@@ -204,7 +205,7 @@
                 },
             },
         },
-        release_avf_enable_remote_attestation: {
+        avf_remote_attestation_enabled: {
             vintf_fragments: [
                 "virtualizationservice.xml",
             ],
@@ -235,7 +236,7 @@
     config_namespace: "ANDROID",
     bool_variables: [
         "release_avf_enable_llpvm_changes",
-        "release_avf_enable_remote_attestation",
+        "avf_remote_attestation_enabled",
     ],
     properties: ["srcs"],
 }
@@ -247,7 +248,7 @@
         release_avf_enable_llpvm_changes: {
             srcs: ["virtualizationservice.rc.llpvm"],
         },
-        release_avf_enable_remote_attestation: {
+        avf_remote_attestation_enabled: {
             srcs: ["virtualizationservice.rc.ra"],
         },
     },
diff --git a/apex/product_packages.mk b/apex/product_packages.mk
index a318817..486334c 100644
--- a/apex/product_packages.mk
+++ b/apex/product_packages.mk
@@ -56,3 +56,9 @@
     $(error RELEASE_AVF_ENABLE_DICE_CHANGES must also be enabled)
   endif
 endif
+
+ifdef RELEASE_AVF_ENABLE_NETWORK
+  ifndef RELEASE_AVF_ENABLE_LLPVM_CHANGES
+    $(error RELEASE_AVF_ENABLE_LLPVM_CHANGES must also be enabled)
+  endif
+endif
diff --git a/apex/sign_virt_apex_test.sh b/apex/sign_virt_apex_test.sh
index e6a892b..e4ac615 100644
--- a/apex/sign_virt_apex_test.sh
+++ b/apex/sign_virt_apex_test.sh
@@ -25,12 +25,16 @@
 DEBUGFS=$TEST_DIR/debugfs_static
 FSCKEROFS=$TEST_DIR/fsck.erofs
 
+echo "Extracting the virt apex ..."
 deapexer --debugfs_path $DEBUGFS --fsckerofs_path $FSCKEROFS \
   extract $TEST_DIR/com.android.virt.apex $TMP_ROOT
 
 if [ "$(ls -A $TMP_ROOT/etc/fs/)" ]; then
-  sign_virt_apex $TEST_DIR/test.com.android.virt.pem $TMP_ROOT
-  sign_virt_apex --verify $TEST_DIR/test.com.android.virt.pem $TMP_ROOT
+  echo "Re-signing the contents ..."
+  sign_virt_apex -v $TEST_DIR/test.com.android.virt.pem $TMP_ROOT
+  echo "Verifying the contents ..."
+  sign_virt_apex -v --verify $TEST_DIR/test.com.android.virt.pem $TMP_ROOT
+  echo "Done."
 else
   echo "No filesystem images. Skip."
 fi
diff --git a/apex/vmnic.rc b/apex/vmnic.rc
index 486f387..f5dfd99 100644
--- a/apex/vmnic.rc
+++ b/apex/vmnic.rc
@@ -13,8 +13,8 @@
 # limitations under the License.
 
 service vmnic /apex/com.android.virt/bin/vmnic
-    user system
-    group system
+    user root
+    group vpn
     interface aidl android.system.virtualizationservice_internal.IVmnic
     disabled
     oneshot
diff --git a/compos/Android.bp b/compos/Android.bp
index b840506..220533a 100644
--- a/compos/Android.bp
+++ b/compos/Android.bp
@@ -25,7 +25,7 @@
         "librpcbinder_rs",
         "librustutils",
         "libscopeguard",
-        "libvm_payload_bindgen",
+        "libvm_payload_rs",
     ],
     prefer_rlib: true,
     shared_libs: [
diff --git a/compos/apk/assets/vm_config.json b/compos/apk/assets/vm_config.json
index 1f5cdba..28e0f07 100644
--- a/compos/apk/assets/vm_config.json
+++ b/compos/apk/assets/vm_config.json
@@ -27,5 +27,6 @@
     }
   ],
   "export_tombstones": true,
-  "enable_authfs": true
+  "enable_authfs": true,
+  "hugepages": true
 }
diff --git a/compos/apk/assets/vm_config_staged.json b/compos/apk/assets/vm_config_staged.json
index 37b1d7a..afc3767 100644
--- a/compos/apk/assets/vm_config_staged.json
+++ b/compos/apk/assets/vm_config_staged.json
@@ -28,5 +28,6 @@
     }
   ],
   "export_tombstones": true,
-  "enable_authfs": true
+  "enable_authfs": true,
+  "hugepages": true
 }
diff --git a/compos/apk/assets/vm_config_system_ext.json b/compos/apk/assets/vm_config_system_ext.json
index 1ef43f0..730f592 100644
--- a/compos/apk/assets/vm_config_system_ext.json
+++ b/compos/apk/assets/vm_config_system_ext.json
@@ -30,5 +30,6 @@
     }
   ],
   "export_tombstones": true,
-  "enable_authfs": true
+  "enable_authfs": true,
+  "hugepages": true
 }
diff --git a/compos/apk/assets/vm_config_system_ext_staged.json b/compos/apk/assets/vm_config_system_ext_staged.json
index 9103a9e..6d91aa2 100644
--- a/compos/apk/assets/vm_config_system_ext_staged.json
+++ b/compos/apk/assets/vm_config_system_ext_staged.json
@@ -31,5 +31,6 @@
     }
   ],
   "export_tombstones": true,
-  "enable_authfs": true
+  "enable_authfs": true,
+  "hugepages": true
 }
diff --git a/compos/common/compos_client.rs b/compos/common/compos_client.rs
index 6914380..107f8d0 100644
--- a/compos/common/compos_client.rs
+++ b/compos/common/compos_client.rs
@@ -36,7 +36,6 @@
 use glob::glob;
 use log::{info, warn};
 use platformproperties::hypervisorproperties;
-use rustutils::system_properties;
 use std::fs::File;
 use std::path::{Path, PathBuf};
 use vmclient::{DeathReason, ErrorCode, VmInstance, VmWaitError};
@@ -80,7 +79,11 @@
         idsig_manifest_ext_apk: &Path,
         parameters: &VmParameters,
     ) -> Result<Self> {
-        let protected_vm = want_protected_vm()?;
+        let have_protected_vm =
+            hypervisorproperties::hypervisor_protected_vm_supported()?.unwrap_or(false);
+        if !have_protected_vm {
+            bail!("Protected VM not supported, unable to start VM");
+        }
 
         let instance_fd = ParcelFileDescriptor::new(instance_image);
 
@@ -133,7 +136,7 @@
             payload: Payload::ConfigPath(config_path),
             debugLevel: debug_level,
             extraIdsigs: extra_idsigs,
-            protectedVm: protected_vm,
+            protectedVm: true,
             memoryMib: parameters.memory_mib.unwrap_or(0), // 0 means use the default
             cpuTopology: cpu_topology,
             customConfig: custom_config,
@@ -236,28 +239,6 @@
     Ok(idsig_fd)
 }
 
-fn want_protected_vm() -> Result<bool> {
-    let have_protected_vm =
-        hypervisorproperties::hypervisor_protected_vm_supported()?.unwrap_or(false);
-    if have_protected_vm {
-        info!("Starting protected VM");
-        return Ok(true);
-    }
-
-    let is_debug_build = system_properties::read("ro.debuggable")?.as_deref().unwrap_or("0") == "1";
-    if !is_debug_build {
-        bail!("Protected VM not supported, unable to start VM");
-    }
-
-    let have_non_protected_vm = hypervisorproperties::hypervisor_vm_supported()?.unwrap_or(false);
-    if have_non_protected_vm {
-        warn!("Protected VM not supported, falling back to non-protected on debuggable build");
-        return Ok(false);
-    }
-
-    bail!("No VM support available")
-}
-
 struct Callback {}
 impl vmclient::VmCallback for Callback {
     fn on_payload_started(&self, cid: i32) {
diff --git a/compos/src/compsvc_main.rs b/compos/src/compsvc_main.rs
index 06cc599..9bc522c 100644
--- a/compos/src/compsvc_main.rs
+++ b/compos/src/compsvc_main.rs
@@ -23,13 +23,9 @@
 mod fsverity;
 
 use anyhow::Result;
-use binder::unstable_api::AsNative;
 use compos_common::COMPOS_VSOCK_PORT;
 use log::{debug, error};
-use std::os::raw::c_void;
 use std::panic;
-use std::ptr;
-use vm_payload_bindgen::{AIBinder, AVmPayload_notifyPayloadReady, AVmPayload_runVsockRpcServer};
 
 fn main() {
     if let Err(e) = try_main() {
@@ -50,17 +46,5 @@
     }));
 
     debug!("compsvc is starting as a rpc service.");
-    let param = ptr::null_mut();
-    let mut service = compsvc::new_binder()?.as_binder();
-    let service = service.as_native_mut() as *mut AIBinder;
-    // 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, COMPOS_VSOCK_PORT, Some(on_ready), param) }
-}
-
-extern "C" fn on_ready(_param: *mut c_void) {
-    // SAFETY: Invokes a method from the bindgen library `vm_payload_bindgen` which is safe to
-    // call at any time.
-    unsafe { AVmPayload_notifyPayloadReady() };
+    vm_payload::run_single_vsock_service(compsvc::new_binder()?, COMPOS_VSOCK_PORT)
 }
diff --git a/compos/tests/java/android/compos/test/ComposTestCase.java b/compos/tests/java/android/compos/test/ComposTestCase.java
index bd011fa..b31f4f3 100644
--- a/compos/tests/java/android/compos/test/ComposTestCase.java
+++ b/compos/tests/java/android/compos/test/ComposTestCase.java
@@ -24,11 +24,13 @@
 import static com.google.common.truth.Truth.assertWithMessage;
 
 import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
 
 import android.platform.test.annotations.RootPermissionTest;
 
 import com.android.microdroid.test.host.CommandRunner;
 import com.android.microdroid.test.host.MicrodroidHostTestCaseBase;
+import com.android.tradefed.device.TestDevice;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.FileInputStreamSource;
 import com.android.tradefed.result.LogDataType;
@@ -85,6 +87,8 @@
         assumeDeviceIsCapable(getDevice());
         // Test takes too long to run on Cuttlefish (b/292824951).
         assumeFalse("Skipping test on Cuttlefish", isCuttlefish());
+        // CompOS requires a protected VM
+        assumeTrue(((TestDevice) getDevice()).supportsMicrodroid(/*protectedVm*/ true));
 
         String value = getDevice().getProperty(SYSTEM_SERVER_COMPILER_FILTER_PROP_NAME);
         if (value == null) {
diff --git a/docs/custom_vm.md b/docs/custom_vm.md
index 0825f06..fce6da2 100644
--- a/docs/custom_vm.md
+++ b/docs/custom_vm.md
@@ -63,27 +63,30 @@
 As of today (April 2024), ChromiumOS is the only officially supported guest
 payload. We will be adding more OSes in the future.
 
-#### Download from build server
+#### Download ChromiumOS from build server
 
-  - Step 1) Go to the link https://ci.chromium.org/ui/p/chromeos/builders/chromiumos/ferrochrome-public-main/
-    - Note: I 'searched' the ferrochrome target with builder search.
-  - Step 2) Click a build number
-  - Step 3) Expand steps and find `48. upload artifacts`.
-  - Step 4) Click `gs upload dir`. You'll see Cloud storage with comprehensive artifacts (e.g. [Here](https://pantheon.corp.google.com/storage/browser/chromiumos-image-archive/ferrochrome-public/R126-15883.0.0) is the initial build of ferrochrome)
-  - Step 5) Download `image.zip`, which contains working vmlinuz.
-    - Note: DO NOT DOWNLOAD `vmlinuz.tar.xz` from the CI.
-  - Step 6) Uncompress `image.zip`, and boot with `chromiumos_test_image.bin` and `boot_images/vmlinuz`.
-    - Note: DO NOT USE `vmlinuz.bin`.
+Download
+https://storage.googleapis.com/chromiumos-image-archive/ferrochrome-public/R128-15926.0.0/chromiumos_test_image.tar.xz.
+The above will download ferrochrome test image with version `R128-15926.0.0`.
 
-IMPORTANT: DO NOT USE `vmlinuz.bin` for passing to crosvm. It doesn't pick-up the correct `init` process (picks `/init` instead of `/sbin/init`, and `cfg80211` keeps crashing (i.e. no network)
+To download latest version, use following code.
 
+```sh
+URL=https://storage.googleapis.com/chromiumos-image-archive/ferrochrome-public
+LATEST_VERSION=$(curl -s ${URL}/LATEST-main)
+curl -O ${URL}/${LATEST_VERSION}/chromiumos_test_image.tar.xz
+```
+
+To navigate build server artifacts,
+[install gsutil](https://cloud.google.com/storage/docs/gsutil_install).
+`gs://chromiumos-image-archive/ferrochrome-public` is the top level directory for ferrochrome build.
 
 #### Build ChromiumOS for VM
 
 First, check out source code from the ChromiumOS and Chromium projects.
 
+* Checking out Chromium: https://www.chromium.org/developers/how-tos/get-the-code/
 * Checking out ChromiumOS: https://www.chromium.org/chromium-os/developer-library/guides/development/developer-guide/
-* Checking out Chromium: https://g3doc.corp.google.com/chrome/chromeos/system_services_team/dev_instructions/g3doc/setup_checkout.md?cl=head
 
 Important: When you are at the step “Set up gclient args” in the Chromium checkout instruction, configure .gclient as follows.
 
@@ -95,9 +98,7 @@
     "url": "https://chromium.googlesource.com/chromium/src.git",
     "managed": False,
     "custom_deps": {},
-    "custom_vars": {
-      "checkout_src_internal": True,
-    },
+    "custom_vars": {},
   },
 ]
 target_os = ['chromeos']
@@ -162,10 +163,7 @@
 
 Don’t forget to call `build-image` afterwards.
 
-You need two outputs:
-
-* ChromiumOS disk image: ~/chromiumos/src/build/images/ferrochrome/latest/chromiumos_test_image.bin
-* The kernel: ~/chromiumos/src/build/images/ferrochrome/latest/boot_images/vmlinuz
+You need ChromiumOS disk image: ~/chromiumos/src/build/images/ferrochrome/latest/chromiumos_test_image.bin
 
 ### Create a guest VM configuration
 
@@ -173,7 +171,6 @@
 
 ```
 $ adb push  ~/chromiumos/src/build/images/ferrochrome/latest/chromiumos_test_image.bin /data/local/tmp/
-$ adb push ~/chromiumos/out/build/ferrochrome/boot/vmlinuz /data/local/tmp/kernel
 ```
 
 Create a VM config file as below.
@@ -182,7 +179,6 @@
 $ cat > vm_config.json; adb push vm_config.json /data/local/tmp
 {
     "name": "cros",
-    "kernel": "/data/local/tmp/kernel",
     "disks": [
         {
             "image": "/data/local/tmp/chromiumos_test_image.bin",
@@ -190,11 +186,16 @@
             "writable": true
         }
     ],
+    "gpu": {
+        "backend": "virglrenderer",
+        "context_types": ["virgl2"]
+    },
     "params": "root=/dev/vda3 rootwait noinitrd ro enforcing=0 cros_debug cros_secure",
     "protected": false,
     "cpu_topology": "match_host",
     "platform_version": "~1.0",
-    "memory_mib" : 8096
+    "memory_mib" : 8096,
+    "console_input_device": "ttyS0"
 }
 ```
 
@@ -205,77 +206,30 @@
 
 ```
 $ adb root
-$ adb shell pm enable com.android.virtualization.vmlauncher/.MainActivity
+$ adb shell pm enable com.android.virtualization.vmlauncher/.MainActivityAlias
 $ adb unroot
 ```
 
-Then execute the below to set up the network. In the future, this step won't be necessary.
-
+If virt apex is Google-signed, you need to enable the app and grant the
+permission to the app.
 ```
-$ cat > setup_network.sh; adb push setup_network.sh /data/local/tmp
-#!/system/bin/sh
-
-set -e
-
-TAP_IFACE=crosvm_tap
-TAP_ADDR=192.168.1.1
-TAP_NET=192.168.1.0
-
-function setup_network() {
-  local WAN_IFACE=$(ip route get 8.8.8.8 2> /dev/null | awk -- '{printf $5}')
-  if [ "${WAN_IFACE}" == "" ]; then
-    echo "No network. Connect to a WiFi network and start again"
-    return 1
-  fi
-
-  if ip link show ${TAP_IFACE} &> /dev/null ; then
-    echo "TAP interface ${TAP_IFACE} already exists"
-    return 1
-  fi
-
-  ip tuntap add mode tap group virtualmachine vnet_hdr ${TAP_IFACE}
-  ip addr add ${TAP_ADDR}/24 dev ${TAP_IFACE}
-  ip link set ${TAP_IFACE} up
-  ip rule flush
-  ip rule add from all lookup ${WAN_IFACE}
-  ip route add ${TAP_NET}/24 dev ${TAP_IFACE} table ${WAN_IFACE}
-  sysctl net.ipv4.ip_forward=1
-  iptables -t filter -F
-  iptables -t nat -A POSTROUTING -s ${TAP_NET}/24 -j MASQUERADE
-}
-
-function setup_if_necessary() {
-  if [ "$(getprop ro.crosvm.network.setup.done)" == 1 ]; then
-    return
-  fi
-  echo "Setting up..."
-  check_privilege
-  setup_network
-  setenforce 0
-  chmod 666 /dev/tun
-  setprop ro.crosvm.network.setup.done 1
-}
-
-function check_privilege() {
-  if [ "$(id -u)" -ne 0 ]; then
-    echo "Run 'adb root' first"
-    return 1
-  fi
-}
-
-setup_if_necessary
-^D
-
-adb root; adb shell /data/local/tmp/setup_network.sh
+$ adb root
+$ adb shell pm enable com.google.android.virtualization.vmlauncher/com.android.virtualization.vmlauncher.MainActivityAlias
+$ adb shell pm grant com.google.android.virtualization.vmlauncher android.permission.USE_CUSTOM_VIRTUAL_MACHINE
+$ adb unroot
 ```
 
-Then, finally tap the VmLauncherApp app from the launcher UI. You will see
+Second, ensure your device is connected to the Internet.
+
+Finally, tap the VmLauncherApp app from the launcher UI. You will see
 Ferrochrome booting!
 
 If it doesn’t work well, try
 
 ```
 $ adb shell pm clear com.android.virtualization.vmlauncher
+# or
+$ adb shell pm clear com.google.android.virtualization.vmlauncher
 ```
 
 ### Inside guest OS (for ChromiumOS only)
@@ -288,4 +242,27 @@
 * DNS: 8.8.8.8 (or any DNS server you know)
 
 These settings are persistent; stored in chromiumos_test_image.bin. So you
-don’t have to repeat this next time.`
+don’t have to repeat this next time.
+
+### Debugging
+
+To open the serial console (interactive terminal):
+```shell
+$ adb shell -t /apex/com.android.virt/bin/vm console
+```
+
+To see console logs only, check
+`/data/data/com.android.virtualization.vmlauncher/files/console.log`
+Or
+`/data/data/com.google.android.virtualization.vmlauncher/files/console.log`
+
+```shell
+$ adb shell su root tail +0 -F /data/data/com{,.google}.android.virtualization.vmlauncher/files/console.log
+```
+
+For ChromiumOS, you can ssh-in. Use following commands after network setup.
+
+```shell
+$ adb kill-server ; adb start-server; adb forward tcp:9222 tcp:9222
+$ ssh -oProxyCommand=none -o UserKnownHostsFile=/dev/null root@localhost -p 9222
+```
diff --git a/docs/vm_remote_attestation.md b/docs/vm_remote_attestation.md
index 835dcac..195804f 100644
--- a/docs/vm_remote_attestation.md
+++ b/docs/vm_remote_attestation.md
@@ -106,3 +106,27 @@
     normal mode.
 -   The `vmComponents` field contains a list of all the APKs and apexes loaded
     by the pVM.
+
+## To Support It
+
+VM remote attestation is a strongly recommended feature from Android V. To support
+it, you only need to provide a valid VM DICE chain satisfying the following
+requirements:
+
+- The DICE chain must have a UDS-rooted public key registered at the RKP factory.
+- The DICE chain should have RKP VM markers that help identify RKP VM as required
+  by the [remote provisioning HAL][rkp-hal-markers].
+
+The feature is enabled by default. To disable it, you have two options:
+
+1. Set `PRODUCT_AVF_REMOTE_ATTESTATION_DISABLED` to `true` in your Makefile to
+   disable the feature at build time.
+
+2. Set the system property `avf.remote_attestation.enabled` to `0` to disable
+   the feature at boot time by including the following line in vendor init:
+   `setprop avf.remote_attestation.enabled 0`.
+
+If you don't set any of these variables, VM remote attestation will be enabled
+by default.
+
+[rkp-hal-markers]: https://android.googlesource.com/platform/hardware/interfaces/+/main/security/rkp/README.md#hal
diff --git a/ferrochrome_app/Android.bp b/ferrochrome_app/Android.bp
new file mode 100644
index 0000000..b3998a7
--- /dev/null
+++ b/ferrochrome_app/Android.bp
@@ -0,0 +1,26 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app {
+    name: "FerrochromeApp",
+    srcs: ["java/**/*.java"],
+    resource_dirs: ["res"],
+    platform_apis: true,
+    // TODO(b/348113995): move this app to product partition
+    system_ext_specific: true,
+    privileged: true,
+    init_rc: ["custom_vm_setup.rc"],
+    required: ["custom_vm_setup"],
+    certificate: "platform",
+    static_libs: [
+        "apache-commons-compress",
+    ],
+}
+
+sh_binary {
+    name: "custom_vm_setup",
+    src: "custom_vm_setup.sh",
+    system_ext_specific: true,
+    host_supported: false,
+}
diff --git a/ferrochrome_app/AndroidManifest.xml b/ferrochrome_app/AndroidManifest.xml
new file mode 100644
index 0000000..d783bbc
--- /dev/null
+++ b/ferrochrome_app/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.virtualization.ferrochrome"
+    android:sharedUserId="android.uid.system" >
+
+    <uses-permission android:name="android.permission.INTERNET" />
+    <application
+        android:label="Ferrochrome">
+        <activity android:name=".FerrochromeActivity"
+                  android:configChanges="orientation|screenSize|keyboard|keyboardHidden|navigation|uiMode"
+                  android:theme="@style/MyTheme"
+                  android:screenOrientation="landscape"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>
diff --git a/ferrochrome_app/custom_vm_setup.rc b/ferrochrome_app/custom_vm_setup.rc
new file mode 100644
index 0000000..f4244b7
--- /dev/null
+++ b/ferrochrome_app/custom_vm_setup.rc
@@ -0,0 +1,23 @@
+# 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.
+
+service custom_vm_setup /system_ext/bin/custom_vm_setup
+    user shell
+    group shell
+    disabled
+    oneshot
+    seclabel u:r:shell:s0
+
+on property:debug.custom_vm_setup.start=true
+    start custom_vm_setup
\ No newline at end of file
diff --git a/ferrochrome_app/custom_vm_setup.sh b/ferrochrome_app/custom_vm_setup.sh
new file mode 100644
index 0000000..92a23a3
--- /dev/null
+++ b/ferrochrome_app/custom_vm_setup.sh
@@ -0,0 +1,13 @@
+#!/system/bin/sh
+
+function copy_files() {
+  cp -u /sdcard/vm_config.json /data/local/tmp
+  cp -u /sdcard/chromiumos_test_image.bin /data/local/tmp
+  chmod 666 /data/local/tmp/vm_config.json
+  chmod 666 /data/local/tmp/chromiumos_test_image.bin
+}
+setprop debug.custom_vm_setup.done false
+copy_files
+pm grant com.google.android.virtualization.vmlauncher android.permission.USE_CUSTOM_VIRTUAL_MACHINE
+setprop debug.custom_vm_setup.start false
+setprop debug.custom_vm_setup.done true
\ No newline at end of file
diff --git a/ferrochrome_app/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java b/ferrochrome_app/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java
new file mode 100644
index 0000000..d9e5229
--- /dev/null
+++ b/ferrochrome_app/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java
@@ -0,0 +1,175 @@
+/*
+ * 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.virtualization.ferrochrome;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.SystemProperties;
+import android.util.Log;
+import android.view.WindowManager;
+import android.widget.TextView;
+
+import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
+import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
+import org.apache.commons.compress.compressors.xz.XZCompressorInputStream;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class FerrochromeActivity extends Activity {
+    ExecutorService executorService = Executors.newSingleThreadExecutor();
+    private static final String TAG = "FerrochromeActivity";
+    private static final String ACTION_VM_LAUNCHER = "android.virtualization.VM_LAUNCHER";
+    private static final String FERROCHROME_VERSION = "R128-15926.0.0";
+    private static final String EXTERNAL_STORAGE_DIR =
+            Environment.getExternalStorageDirectory().getPath() + File.separator;
+    private static final Path IMAGE_PATH =
+            Path.of(EXTERNAL_STORAGE_DIR + "chromiumos_test_image.bin");
+    private static final Path IMAGE_VERSION_INFO =
+            Path.of(EXTERNAL_STORAGE_DIR + "ferrochrome_image_version");
+    private static final Path VM_CONFIG_PATH = Path.of(EXTERNAL_STORAGE_DIR + "vm_config.json");
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_ferrochrome);
+        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+
+        // Find VM Launcher
+        Intent intent = new Intent(ACTION_VM_LAUNCHER);
+        PackageManager pm = getPackageManager();
+        List<ResolveInfo> resolveInfos =
+                pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
+        if (resolveInfos == null || resolveInfos.size() != 1) {
+            updateStatus("Failed to resolve VM Launcher");
+            return;
+        }
+
+        // Clean up the existing vm launcher process if there is
+        ActivityManager am = getSystemService(ActivityManager.class);
+        am.killBackgroundProcesses(resolveInfos.get(0).activityInfo.packageName);
+
+        executorService.execute(
+                () -> {
+                    if (Files.notExists(IMAGE_PATH)
+                            || !FERROCHROME_VERSION.equals(getVersionInfo())) {
+                        updateStatus("Starting first-time setup.");
+                        updateStatus(
+                                "Downloading Ferrochrome image. This can take about 5 to 10"
+                                        + " minutes, depending on your network speed.");
+                        if (download(FERROCHROME_VERSION)) {
+                            updateStatus("Done.");
+                        } else {
+                            updateStatus(
+                                    "Download failed. Check the internet connection and retry.");
+                            return;
+                        }
+                    } else {
+                        updateStatus("Ferrochrome is already downloaded.");
+                    }
+                    updateStatus("Updating VM config.");
+                    copyVmConfigJson();
+                    updateStatus("Updating VM images. This may take a few minutes.");
+                    SystemProperties.set("debug.custom_vm_setup.start", "true");
+                    while (!SystemProperties.getBoolean("debug.custom_vm_setup.done", false)) {
+                        // Wait for custom_vm_setup
+                        try {
+                            Thread.sleep(1000);
+                        } catch (Exception e) {
+                            Log.d(TAG, e.toString());
+                        }
+                    }
+                    updateStatus("Done.");
+                    updateStatus("Starting Ferrochrome...");
+                    runOnUiThread(() -> startActivity(intent));
+                });
+    }
+
+    private void updateStatus(String line) {
+        Log.d(TAG, line);
+        runOnUiThread(
+                () -> {
+                    TextView statusView = findViewById(R.id.status_txt_view);
+                    statusView.append(line + "\n");
+                });
+    }
+
+    private void copyVmConfigJson() {
+        try (InputStream is = getResources().openRawResource(R.raw.vm_config)) {
+            Files.copy(is, VM_CONFIG_PATH, StandardCopyOption.REPLACE_EXISTING);
+        } catch (IOException e) {
+            updateStatus(e.toString());
+        }
+    }
+
+    private String getVersionInfo() {
+        try {
+            return new String(Files.readAllBytes(IMAGE_VERSION_INFO), StandardCharsets.UTF_8);
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
+    private boolean updateVersionInfo(String version) {
+        try {
+            Files.write(IMAGE_VERSION_INFO, version.getBytes(StandardCharsets.UTF_8));
+        } catch (IOException e) {
+            Log.d(TAG, e.toString());
+        }
+        return true;
+    }
+
+    private boolean download(String version) {
+        String urlString =
+                "https://storage.googleapis.com/chromiumos-image-archive/ferrochrome-public/"
+                        + version
+                        + "/chromiumos_test_image.tar.xz";
+        try (InputStream is = (new URL(urlString)).openStream();
+                XZCompressorInputStream xz = new XZCompressorInputStream(is);
+                TarArchiveInputStream tar = new TarArchiveInputStream(xz)) {
+            TarArchiveEntry entry;
+            while ((entry = tar.getNextTarEntry()) != null) {
+                if (!entry.getName().contains("chromiumos_test_image.bin")) {
+                    continue;
+                }
+                updateStatus("copy " + entry.getName() + " start");
+                Files.copy(tar, IMAGE_PATH, StandardCopyOption.REPLACE_EXISTING);
+                updateStatus("copy " + entry.getName() + " done");
+                updateVersionInfo(version);
+                break;
+            }
+        } catch (Exception e) {
+            updateStatus(e.toString());
+            return false;
+        }
+        return true;
+    }
+}
diff --git a/ferrochrome_app/res/layout/activity_ferrochrome.xml b/ferrochrome_app/res/layout/activity_ferrochrome.xml
new file mode 100644
index 0000000..7d5e8aa
--- /dev/null
+++ b/ferrochrome_app/res/layout/activity_ferrochrome.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".FerrochromeActivity">
+  <TextView
+      android:layout_width="match_parent"
+      android:layout_height="match_parent"
+      android:id="@+id/status_txt_view"/>
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/ferrochrome_app/res/raw/vm_config.json b/ferrochrome_app/res/raw/vm_config.json
new file mode 100644
index 0000000..d79400c
--- /dev/null
+++ b/ferrochrome_app/res/raw/vm_config.json
@@ -0,0 +1,20 @@
+{
+    "name": "cros",
+    "disks": [
+        {
+            "image": "/data/local/tmp/chromiumos_test_image.bin",
+            "partitions": [],
+            "writable": true
+        }
+    ],
+    "params": "root=/dev/vda3 rootwait noinitrd ro enforcing=0 cros_debug cros_secure",
+    "protected": false,
+    "cpu_topology": "match_host",
+    "platform_version": "~1.0",
+    "memory_mib": 8096,
+    "gpu": {
+        "backend": "virglrenderer",
+        "context_types": ["virgl2"]
+    },
+    "console_input_device": "ttyS0"
+}
\ No newline at end of file
diff --git a/ferrochrome_app/res/values/themes.xml b/ferrochrome_app/res/values/themes.xml
new file mode 100644
index 0000000..c9a9ed2
--- /dev/null
+++ b/ferrochrome_app/res/values/themes.xml
@@ -0,0 +1,4 @@
+<resources xmlns:tools="http://schemas.android.com/tools">
+    <style name="MyTheme" parent="@android:style/Theme.DeviceDefault.NoActionBar">
+    </style>
+</resources>
diff --git a/java/framework/api/test-current.txt b/java/framework/api/test-current.txt
index d20d543..7e8da26 100644
--- a/java/framework/api/test-current.txt
+++ b/java/framework/api/test-current.txt
@@ -31,6 +31,7 @@
     field @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") public static final String FEATURE_DICE_CHANGES = "com.android.kvm.DICE_CHANGES";
     field @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") public static final String FEATURE_LLPVM_CHANGES = "com.android.kvm.LLPVM_CHANGES";
     field @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") public static final String FEATURE_MULTI_TENANT = "com.android.kvm.MULTI_TENANT";
+    field public static final String FEATURE_NETWORK = "com.android.kvm.NETWORK";
     field @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") public static final String FEATURE_REMOTE_ATTESTATION = "com.android.kvm.REMOTE_ATTESTATION";
     field @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") public static final String FEATURE_VENDOR_MODULES = "com.android.kvm.VENDOR_MODULES";
   }
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachine.java b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
index 2f6e306..bca36a4 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachine.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
@@ -42,8 +42,8 @@
 
 import static java.util.Objects.requireNonNull;
 
-import android.annotation.FlaggedApi;
 import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
@@ -63,8 +63,8 @@
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
+import android.system.ErrnoException;
+import android.system.OsConstants;
 import android.system.virtualizationcommon.DeathReason;
 import android.system.virtualizationcommon.ErrorCode;
 import android.system.virtualizationservice.IVirtualMachine;
@@ -78,13 +78,17 @@
 import android.system.virtualizationservice.VirtualMachineState;
 import android.util.JsonReader;
 import android.util.Log;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.system.virtualmachine.flags.Flags;
 
 import libcore.io.IoBridge;
+import libcore.io.IoUtils;
 
 import java.io.File;
+import java.io.FileDescriptor;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
@@ -97,17 +101,20 @@
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.nio.channels.FileChannel;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.FileAlreadyExistsException;
 import java.nio.file.FileVisitResult;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.SimpleFileVisitor;
 import java.nio.file.attribute.BasicFileAttributes;
-import java.util.Arrays;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Consumer;
 import java.util.zip.ZipFile;
@@ -320,6 +327,10 @@
 
     private final boolean mVmConsoleInputSupported;
 
+    private final boolean mConnectVmConsole;
+
+    private final Executor mConsoleExecutor = Executors.newSingleThreadExecutor();
+
     /** The configuration that is currently associated with this VM. */
     @GuardedBy("mLock")
     @NonNull
@@ -348,6 +359,26 @@
 
     @GuardedBy("mLock")
     @Nullable
+    private ParcelFileDescriptor mTeeConsoleOutReader;
+
+    @GuardedBy("mLock")
+    @Nullable
+    private ParcelFileDescriptor mTeeConsoleOutWriter;
+
+    @GuardedBy("mLock")
+    @Nullable
+    private ParcelFileDescriptor mPtyFd;
+
+    @GuardedBy("mLock")
+    @Nullable
+    private ParcelFileDescriptor mPtsFd;
+
+    @GuardedBy("mLock")
+    @Nullable
+    private String mPtsName;
+
+    @GuardedBy("mLock")
+    @Nullable
     private ParcelFileDescriptor mLogReader;
 
     @GuardedBy("mLock")
@@ -417,6 +448,7 @@
 
         mVmOutputCaptured = config.isVmOutputCaptured();
         mVmConsoleInputSupported = config.isVmConsoleInputSupported();
+        mConnectVmConsole = config.isConnectVmConsole();
     }
 
     /**
@@ -892,6 +924,11 @@
         }
         rawConfig.inputDevices = inputDevices.toArray(new InputDevice[0]);
 
+        // Handle network support
+        if (vmConfig.getCustomImageConfig() != null) {
+            rawConfig.networkSupported = vmConfig.getCustomImageConfig().useNetwork();
+        }
+
         return android.system.virtualizationservice.VirtualMachineConfig.rawConfig(rawConfig);
     }
 
@@ -1100,7 +1137,9 @@
     /**
      * Runs this virtual machine. The returning of this method however doesn't mean that the VM has
      * actually started running or the OS has booted there. Such events can be notified by
-     * registering a callback using {@link #setCallback} before calling {@code run()}.
+     * registering a callback using {@link #setCallback} before calling {@code run()}. There is no
+     * limit other than available memory that limits the number of virtual machines that can run at
+     * the same time.
      *
      * <p>NOTE: This method may block and should not be called on the main thread.
      *
@@ -1128,6 +1167,10 @@
             IVirtualizationService service = mVirtualizationService.getBinder();
 
             try {
+                if (mConnectVmConsole) {
+                    createPtyConsole();
+                }
+
                 if (mVmOutputCaptured) {
                     createVmOutputPipes();
                 }
@@ -1136,6 +1179,38 @@
                     createVmInputPipes();
                 }
 
+                ParcelFileDescriptor consoleOutFd = null;
+                if (mConnectVmConsole && mVmOutputCaptured) {
+                    // If we are enabling output pipes AND the host console, then we tee the console
+                    // output to both.
+                    ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
+                    mTeeConsoleOutReader = pipe[0];
+                    mTeeConsoleOutWriter = pipe[1];
+                    consoleOutFd = mTeeConsoleOutWriter;
+                    TeeWorker tee =
+                            new TeeWorker(
+                                    mName + " console",
+                                    new FileInputStream(mTeeConsoleOutReader.getFileDescriptor()),
+                                    List.of(
+                                            new FileOutputStream(mPtyFd.getFileDescriptor()),
+                                            new FileOutputStream(
+                                                    mConsoleOutWriter.getFileDescriptor())));
+                    // If the VM is stopped then the tee worker thread would get an EOF or read()
+                    // error which would tear down itself.
+                    mConsoleExecutor.execute(tee);
+                } else if (mConnectVmConsole) {
+                    consoleOutFd = mPtyFd;
+                } else if (mVmOutputCaptured) {
+                    consoleOutFd = mConsoleOutWriter;
+                }
+
+                ParcelFileDescriptor consoleInFd = null;
+                if (mConnectVmConsole) {
+                    consoleInFd = mPtyFd;
+                } else if (mVmConsoleInputSupported) {
+                    consoleInFd = mConsoleInReader;
+                }
+
                 VirtualMachineConfig vmConfig = getConfig();
                 android.system.virtualizationservice.VirtualMachineConfig vmConfigParcel =
                         vmConfig.getCustomImageConfig() != null
@@ -1143,10 +1218,12 @@
                                 : createVirtualMachineConfigForAppFrom(vmConfig, service);
 
                 mVirtualMachine =
-                        service.createVm(
-                                vmConfigParcel, mConsoleOutWriter, mConsoleInReader, mLogWriter);
+                        service.createVm(vmConfigParcel, consoleOutFd, consoleInFd, mLogWriter);
                 mVirtualMachine.registerCallback(new CallbackTranslator(service));
                 mContext.registerComponentCallbacks(mMemoryManagementCallbacks);
+                if (mConnectVmConsole) {
+                    mVirtualMachine.setHostConsoleName(getHostConsoleName());
+                }
                 mVirtualMachine.start();
             } catch (IOException e) {
                 throw new VirtualMachineException("failed to persist files", e);
@@ -1203,15 +1280,81 @@
     private void createVmInputPipes() throws VirtualMachineException {
         try {
             if (mConsoleInReader == null || mConsoleInWriter == null) {
-                ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
-                mConsoleInReader = pipe[0];
-                mConsoleInWriter = pipe[1];
+                if (mConnectVmConsole) {
+                    // If we are enabling input pipes AND the host console, then we should just use
+                    // the host pty peer end as the console write end.
+                    createPtyConsole();
+                    mConsoleInReader = mPtyFd.dup();
+                    mConsoleInWriter = mPtsFd.dup();
+                } else {
+                    ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
+                    mConsoleInReader = pipe[0];
+                    mConsoleInWriter = pipe[1];
+                }
             }
         } catch (IOException e) {
             throw new VirtualMachineException("Failed to create input stream for VM", e);
         }
     }
 
+    @FunctionalInterface
+    private static interface OpenPtyCallback {
+        public void apply(FileDescriptor mfd, FileDescriptor sfd, byte[] name);
+    }
+
+    // Opens a pty and set the master end to raw mode and O_NONBLOCK.
+    private static native void nativeOpenPtyRawNonblock(OpenPtyCallback resultCallback)
+            throws IOException;
+
+    @GuardedBy("mLock")
+    private void createPtyConsole() throws VirtualMachineException {
+        if (mPtyFd != null && mPtsFd != null) {
+            return;
+        }
+        List<FileDescriptor> fd = new ArrayList<>(2);
+        StringBuilder nameBuilder = new StringBuilder();
+        try {
+            try {
+                nativeOpenPtyRawNonblock(
+                        (FileDescriptor mfd, FileDescriptor sfd, byte[] ptsName) -> {
+                            fd.add(mfd);
+                            fd.add(sfd);
+                            nameBuilder.append(new String(ptsName, StandardCharsets.UTF_8));
+                        });
+            } catch (Exception e) {
+                fd.forEach(IoUtils::closeQuietly);
+                throw e;
+            }
+        } catch (IOException e) {
+            throw new VirtualMachineException(
+                    "Failed to create host console to connect to the VM console", e);
+        }
+        mPtyFd = new ParcelFileDescriptor(fd.get(0));
+        mPtsFd = new ParcelFileDescriptor(fd.get(1));
+        mPtsName = nameBuilder.toString();
+        Log.d(TAG, "Serial console device: " + mPtsName);
+    }
+
+    /**
+     * Returns the name of the peer end (ptsname) of the host console. The host console is only
+     * available if the {@link VirtualMachineConfig} specifies that a host console should
+     * {@linkplain VirtualMachineConfig#isConnectVmConsole connect} to the VM console.
+     *
+     * @throws VirtualMachineException if the host pseudoterminal could not be created, or
+     *     connecting to the VM console is not enabled.
+     * @hide
+     */
+    @NonNull
+    private String getHostConsoleName() throws VirtualMachineException {
+        if (!mConnectVmConsole) {
+            throw new VirtualMachineException("Host console is not enabled");
+        }
+        synchronized (mLock) {
+            createPtyConsole();
+            return mPtsName;
+        }
+    }
+
     /**
      * Returns the stream object representing the console output from the virtual machine. The
      * console output is only available if the {@link VirtualMachineConfig} specifies that it should
@@ -1811,4 +1954,61 @@
             }
         }
     }
+
+    /**
+     * Duplicates {@code InputStream} data to multiple {@code OutputStream}. Like the "tee" command.
+     *
+     * <p>Supports non-blocking writes to the output streams by ignoring EAGAIN error.
+     */
+    private static class TeeWorker implements Runnable {
+        private final String mName;
+        private final InputStream mIn;
+        private final List<OutputStream> mOuts;
+
+        TeeWorker(String name, InputStream in, Collection<OutputStream> outs) {
+            mName = name;
+            mIn = in;
+            mOuts = new ArrayList<>(outs);
+        }
+
+        @Override
+        public void run() {
+            byte[] buffer = new byte[2048];
+            try {
+                while (!Thread.interrupted()) {
+                    int len = mIn.read(buffer);
+                    if (len < 0) {
+                        break;
+                    }
+                    for (OutputStream out : mOuts) {
+                        try {
+                            out.write(buffer, 0, len);
+                        } catch (IOException e) {
+                            // EAGAIN is expected because the file description has O_NONBLOCK flag.
+                            if (!isErrnoError(e, OsConstants.EAGAIN)) {
+                                throw e;
+                            }
+                        }
+                    }
+                }
+            } catch (Exception e) {
+                Log.e(TAG, "Tee " + mName, e);
+            }
+        }
+
+        private static ErrnoException asErrnoException(Throwable e) {
+            if (e instanceof ErrnoException) {
+                return (ErrnoException) e;
+            } else if (e instanceof IOException) {
+                // Try to unwrap ErrnoException#rethrowAsIOException()
+                return asErrnoException(e.getCause());
+            }
+            return null;
+        }
+
+        private static boolean isErrnoError(Exception e, int expectedValue) {
+            ErrnoException errno = asErrnoException(e);
+            return errno != null && errno.errno == expectedValue;
+        }
+    }
 }
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java b/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
index 1b915cd..e18aca2 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -48,7 +48,6 @@
 
 import com.android.system.virtualmachine.flags.Flags;
 
-
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
@@ -78,9 +77,11 @@
     private static final String TAG = "VirtualMachineConfig";
 
     private static String[] EMPTY_STRING_ARRAY = {};
+    private static final String U_BOOT_PREBUILT_PATH = "/apex/com.android.virt/etc/u-boot.bin";
 
     // These define the schema of the config file persisted on disk.
-    private static final int VERSION = 8;
+    // Please bump up the version number when adding a new key.
+    private static final int VERSION = 10;
     private static final String KEY_VERSION = "version";
     private static final String KEY_PACKAGENAME = "packageName";
     private static final String KEY_APKPATH = "apkPath";
@@ -91,12 +92,16 @@
     private static final String KEY_PROTECTED_VM = "protectedVm";
     private static final String KEY_MEMORY_BYTES = "memoryBytes";
     private static final String KEY_CPU_TOPOLOGY = "cpuTopology";
+    private static final String KEY_CONSOLE_INPUT_DEVICE = "consoleInputDevice";
     private static final String KEY_ENCRYPTED_STORAGE_BYTES = "encryptedStorageBytes";
     private static final String KEY_VM_OUTPUT_CAPTURED = "vmOutputCaptured";
     private static final String KEY_VM_CONSOLE_INPUT_SUPPORTED = "vmConsoleInputSupported";
+    private static final String KEY_CONNECT_VM_CONSOLE = "connectVmConsole";
     private static final String KEY_VENDOR_DISK_IMAGE_PATH = "vendorDiskImagePath";
     private static final String KEY_OS = "os";
     private static final String KEY_EXTRA_APKS = "extraApks";
+    private static final String KEY_SHOULD_BOOST_UCLAMP = "shouldBoostUclamp";
+    private static final String KEY_SHOULD_USE_HUGEPAGES = "shouldUseHugepages";
 
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
@@ -173,6 +178,9 @@
     /** CPU topology configuration of the VM. */
     @CpuTopology private final int mCpuTopology;
 
+    /** The serial device for VM console input. */
+    @Nullable private final String mConsoleInputDevice;
+
     /**
      * Path within the APK to the payload config file that defines software aspects of the VM.
      */
@@ -193,11 +201,18 @@
     /** Whether the app can write console input to the VM */
     private final boolean mVmConsoleInputSupported;
 
+    /** Whether to connect the VM console to a host console. */
+    private final boolean mConnectVmConsole;
+
     @Nullable private final File mVendorDiskImage;
 
     /** OS name of the VM using payload binaries. */
     @NonNull @OsName private final String mOs;
 
+    private final boolean mShouldBoostUclamp;
+
+    private final boolean mShouldUseHugepages;
+
     @Retention(RetentionPolicy.SOURCE)
     @StringDef(
             prefix = "MICRODROID",
@@ -226,11 +241,15 @@
             boolean protectedVm,
             long memoryBytes,
             @CpuTopology int cpuTopology,
+            @Nullable String consoleInputDevice,
             long encryptedStorageBytes,
             boolean vmOutputCaptured,
             boolean vmConsoleInputSupported,
+            boolean connectVmConsole,
             @Nullable File vendorDiskImage,
-            @NonNull @OsName String os) {
+            @NonNull @OsName String os,
+            boolean shouldBoostUclamp,
+            boolean shouldUseHugepages) {
         // This is only called from Builder.build(); the builder handles parameter validation.
         mPackageName = packageName;
         mApkPath = apkPath;
@@ -246,11 +265,15 @@
         mProtectedVm = protectedVm;
         mMemoryBytes = memoryBytes;
         mCpuTopology = cpuTopology;
+        mConsoleInputDevice = consoleInputDevice;
         mEncryptedStorageBytes = encryptedStorageBytes;
         mVmOutputCaptured = vmOutputCaptured;
         mVmConsoleInputSupported = vmConsoleInputSupported;
+        mConnectVmConsole = connectVmConsole;
         mVendorDiskImage = vendorDiskImage;
         mOs = os;
+        mShouldBoostUclamp = shouldBoostUclamp;
+        mShouldUseHugepages = shouldUseHugepages;
     }
 
     /** Loads a config from a file. */
@@ -325,12 +348,17 @@
             builder.setMemoryBytes(memoryBytes);
         }
         builder.setCpuTopology(b.getInt(KEY_CPU_TOPOLOGY));
+        String consoleInputDevice = b.getString(KEY_CONSOLE_INPUT_DEVICE);
+        if (consoleInputDevice != null) {
+            builder.setConsoleInputDevice(consoleInputDevice);
+        }
         long encryptedStorageBytes = b.getLong(KEY_ENCRYPTED_STORAGE_BYTES);
         if (encryptedStorageBytes != 0) {
             builder.setEncryptedStorageBytes(encryptedStorageBytes);
         }
         builder.setVmOutputCaptured(b.getBoolean(KEY_VM_OUTPUT_CAPTURED));
         builder.setVmConsoleInputSupported(b.getBoolean(KEY_VM_CONSOLE_INPUT_SUPPORTED));
+        builder.setConnectVmConsole(b.getBoolean(KEY_CONNECT_VM_CONSOLE));
 
         String vendorDiskImagePath = b.getString(KEY_VENDOR_DISK_IMAGE_PATH);
         if (vendorDiskImagePath != null) {
@@ -346,6 +374,9 @@
             }
         }
 
+        builder.setShouldBoostUclamp(b.getBoolean(KEY_SHOULD_BOOST_UCLAMP));
+        builder.setShouldUseHugepages(b.getBoolean(KEY_SHOULD_USE_HUGEPAGES));
+
         return builder.build();
     }
 
@@ -376,6 +407,9 @@
         b.putInt(KEY_DEBUGLEVEL, mDebugLevel);
         b.putBoolean(KEY_PROTECTED_VM, mProtectedVm);
         b.putInt(KEY_CPU_TOPOLOGY, mCpuTopology);
+        if (mConsoleInputDevice != null) {
+            b.putString(KEY_CONSOLE_INPUT_DEVICE, mConsoleInputDevice);
+        }
         if (mMemoryBytes > 0) {
             b.putLong(KEY_MEMORY_BYTES, mMemoryBytes);
         }
@@ -384,6 +418,7 @@
         }
         b.putBoolean(KEY_VM_OUTPUT_CAPTURED, mVmOutputCaptured);
         b.putBoolean(KEY_VM_CONSOLE_INPUT_SUPPORTED, mVmConsoleInputSupported);
+        b.putBoolean(KEY_CONNECT_VM_CONSOLE, mConnectVmConsole);
         if (mVendorDiskImage != null) {
             b.putString(KEY_VENDOR_DISK_IMAGE_PATH, mVendorDiskImage.getAbsolutePath());
         }
@@ -392,6 +427,8 @@
             String[] extraApks = mExtraApks.toArray(new String[0]);
             b.putStringArray(KEY_EXTRA_APKS, extraApks);
         }
+        b.putBoolean(KEY_SHOULD_BOOST_UCLAMP, mShouldBoostUclamp);
+        b.putBoolean(KEY_SHOULD_USE_HUGEPAGES, mShouldUseHugepages);
         b.writeToStream(output);
     }
 
@@ -544,6 +581,16 @@
     }
 
     /**
+     * Returns whether to connect the VM console to a host console.
+     *
+     * @see Builder#setConnectVmConsole
+     * @hide
+     */
+    public boolean isConnectVmConsole() {
+        return mConnectVmConsole;
+    }
+
+    /**
      * Returns the OS of the VM.
      *
      * @see Builder#setOs
@@ -577,6 +624,8 @@
                 && this.mEncryptedStorageBytes == other.mEncryptedStorageBytes
                 && this.mVmOutputCaptured == other.mVmOutputCaptured
                 && this.mVmConsoleInputSupported == other.mVmConsoleInputSupported
+                && this.mConnectVmConsole == other.mConnectVmConsole
+                && this.mConsoleInputDevice == other.mConsoleInputDevice
                 && (this.mVendorDiskImage == null) == (other.mVendorDiskImage == null)
                 && Objects.equals(this.mPayloadConfigPath, other.mPayloadConfigPath)
                 && Objects.equals(this.mPayloadBinaryName, other.mPayloadBinaryName)
@@ -621,6 +670,11 @@
                 Optional.ofNullable(customImageConfig.getBootloaderPath())
                         .map((path) -> openOrNull(new File(path), MODE_READ_ONLY))
                         .orElse(null);
+
+        if (config.kernel == null && config.bootloader == null) {
+            config.bootloader = openOrNull(new File(U_BOOT_PREBUILT_PATH), MODE_READ_ONLY);
+        }
+
         config.params =
                 Optional.ofNullable(customImageConfig.getParams())
                         .map((params) -> TextUtils.join(" ", params))
@@ -645,9 +699,14 @@
                 Optional.ofNullable(customImageConfig.getDisplayConfig())
                         .map(dc -> dc.toParcelable())
                         .orElse(null);
+        config.gpuConfig =
+                Optional.ofNullable(customImageConfig.getGpuConfig())
+                        .map(dc -> dc.toParcelable())
+                        .orElse(null);
         config.protectedVm = this.mProtectedVm;
         config.memoryMib = bytesToMebiBytes(mMemoryBytes);
         config.cpuTopology = (byte) this.mCpuTopology;
+        config.consoleInputDevice = mConsoleInputDevice;
         config.devices = EMPTY_STRING_ARRAY;
         config.platformVersion = "~1.0";
         return config;
@@ -700,6 +759,7 @@
                 vsConfig.cpuTopology = android.system.virtualizationservice.CpuTopology.ONE_CPU;
                 break;
         }
+
         if (mVendorDiskImage != null) {
             VirtualMachineAppConfig.CustomConfig customConfig =
                     new VirtualMachineAppConfig.CustomConfig();
@@ -714,6 +774,10 @@
             }
             vsConfig.customConfig = customConfig;
         }
+
+        vsConfig.boostUclamp = mShouldBoostUclamp;
+        vsConfig.hugePages = mShouldUseHugepages;
+
         return vsConfig;
     }
 
@@ -786,11 +850,15 @@
         private boolean mProtectedVmSet;
         private long mMemoryBytes;
         @CpuTopology private int mCpuTopology = CPU_TOPOLOGY_ONE_CPU;
+        @Nullable private String mConsoleInputDevice;
         private long mEncryptedStorageBytes;
         private boolean mVmOutputCaptured = false;
         private boolean mVmConsoleInputSupported = false;
+        private boolean mConnectVmConsole = false;
         @Nullable private File mVendorDiskImage;
         @NonNull @OsName private String mOs = DEFAULT_OS;
+        private boolean mShouldBoostUclamp = false;
+        private boolean mShouldUseHugepages = false;
 
         /**
          * Creates a builder for the given context.
@@ -862,6 +930,11 @@
                 throw new IllegalStateException("debug level must be FULL to use console input");
             }
 
+            if (mConnectVmConsole && mDebugLevel != DEBUG_LEVEL_FULL) {
+                throw new IllegalStateException(
+                        "debug level must be FULL to connect to the console");
+            }
+
             return new VirtualMachineConfig(
                     packageName,
                     apkPath,
@@ -873,11 +946,15 @@
                     mProtectedVm,
                     mMemoryBytes,
                     mCpuTopology,
+                    mConsoleInputDevice,
                     mEncryptedStorageBytes,
                     mVmOutputCaptured,
                     mVmConsoleInputSupported,
+                    mConnectVmConsole,
                     mVendorDiskImage,
-                    mOs);
+                    mOs,
+                    mShouldBoostUclamp,
+                    mShouldUseHugepages);
         }
 
         /**
@@ -1055,6 +1132,17 @@
         }
 
         /**
+         * Sets the serial device for VM console input.
+         *
+         * @see android.system.virtualizationservice.ConsoleInputDevice
+         * @hide
+         */
+        public Builder setConsoleInputDevice(@Nullable String consoleInputDevice) {
+            mConsoleInputDevice = consoleInputDevice;
+            return this;
+        }
+
+        /**
          * Sets the size (in bytes) of encrypted storage available to the VM. If not set, no
          * encrypted storage is provided.
          *
@@ -1125,6 +1213,23 @@
         }
 
         /**
+         * Sets whether to connect the VM console to a host console. Default is {@code false}.
+         *
+         * <p>Setting this as {@code true} will allow the shell to directly communicate with the VM
+         * console through the connected host console.
+         *
+         * <p>The {@linkplain #setDebugLevel debug level} must be {@link #DEBUG_LEVEL_FULL} to be
+         * set as true.
+         *
+         * @hide
+         */
+        @NonNull
+        public Builder setConnectVmConsole(boolean supported) {
+            mConnectVmConsole = supported;
+            return this;
+        }
+
+        /**
          * Sets the path to the disk image with vendor-specific modules.
          *
          * @hide
@@ -1154,5 +1259,17 @@
             mOs = requireNonNull(os, "os must not be null");
             return this;
         }
+
+        /** @hide */
+        public Builder setShouldBoostUclamp(boolean shouldBoostUclamp) {
+            mShouldBoostUclamp = shouldBoostUclamp;
+            return this;
+        }
+
+        /** @hide */
+        public Builder setShouldUseHugepages(boolean shouldUseHugepages) {
+            mShouldUseHugepages = shouldUseHugepages;
+            return this;
+        }
     }
 }
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java b/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
index 2fcad20..8d4886a 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
@@ -36,6 +36,8 @@
     private static final String KEY_TOUCH = "touch";
     private static final String KEY_KEYBOARD = "keyboard";
     private static final String KEY_MOUSE = "mouse";
+    private static final String KEY_NETWORK = "network";
+    private static final String KEY_GPU = "gpu";
 
     @Nullable private final String name;
     @Nullable private final String kernelPath;
@@ -47,6 +49,8 @@
     private final boolean touch;
     private final boolean keyboard;
     private final boolean mouse;
+    private final boolean network;
+    @Nullable private final GpuConfig gpuConfig;
 
     @Nullable
     public Disk[] getDisks() {
@@ -90,6 +94,10 @@
         return mouse;
     }
 
+    public boolean useNetwork() {
+        return network;
+    }
+
     /** @hide */
     public VirtualMachineCustomImageConfig(
             String name,
@@ -101,7 +109,9 @@
             DisplayConfig displayConfig,
             boolean touch,
             boolean keyboard,
-            boolean mouse) {
+            boolean mouse,
+            boolean network,
+            GpuConfig gpuConfig) {
         this.name = name;
         this.kernelPath = kernelPath;
         this.initrdPath = initrdPath;
@@ -112,6 +122,8 @@
         this.touch = touch;
         this.keyboard = keyboard;
         this.mouse = mouse;
+        this.network = network;
+        this.gpuConfig = gpuConfig;
     }
 
     static VirtualMachineCustomImageConfig from(PersistableBundle customImageConfigBundle) {
@@ -142,6 +154,8 @@
         builder.useTouch(customImageConfigBundle.getBoolean(KEY_TOUCH));
         builder.useKeyboard(customImageConfigBundle.getBoolean(KEY_KEYBOARD));
         builder.useMouse(customImageConfigBundle.getBoolean(KEY_MOUSE));
+        builder.useNetwork(customImageConfigBundle.getBoolean(KEY_NETWORK));
+        builder.setGpuConfig(GpuConfig.from(customImageConfigBundle.getPersistableBundle(KEY_GPU)));
         return builder.build();
     }
 
@@ -173,6 +187,10 @@
         pb.putBoolean(KEY_TOUCH, touch);
         pb.putBoolean(KEY_KEYBOARD, keyboard);
         pb.putBoolean(KEY_MOUSE, mouse);
+        pb.putBoolean(KEY_NETWORK, network);
+        pb.putPersistableBundle(
+                KEY_GPU,
+                Optional.ofNullable(gpuConfig).map(gc -> gc.toPersistableBundle()).orElse(null));
         return pb;
     }
 
@@ -181,6 +199,11 @@
         return displayConfig;
     }
 
+    @Nullable
+    public GpuConfig getGpuConfig() {
+        return gpuConfig;
+    }
+
     /** @hide */
     public static final class Disk {
         private final boolean writable;
@@ -224,6 +247,8 @@
         private boolean touch;
         private boolean keyboard;
         private boolean mouse;
+        private boolean network;
+        private GpuConfig gpuConfig;
 
         /** @hide */
         public Builder() {}
@@ -271,6 +296,12 @@
         }
 
         /** @hide */
+        public Builder setGpuConfig(GpuConfig gpuConfig) {
+            this.gpuConfig = gpuConfig;
+            return this;
+        }
+
+        /** @hide */
         public Builder useTouch(boolean touch) {
             this.touch = touch;
             return this;
@@ -289,6 +320,12 @@
         }
 
         /** @hide */
+        public Builder useNetwork(boolean network) {
+            this.network = network;
+            return this;
+        }
+
+        /** @hide */
         public VirtualMachineCustomImageConfig build() {
             return new VirtualMachineCustomImageConfig(
                     this.name,
@@ -300,7 +337,9 @@
                     displayConfig,
                     touch,
                     keyboard,
-                    mouse);
+                    mouse,
+                    network,
+                    gpuConfig);
         }
     }
 
@@ -437,4 +476,223 @@
             }
         }
     }
+
+    /** @hide */
+    public static final class GpuConfig {
+        private static final String KEY_BACKEND = "backend";
+        private static final String KEY_CONTEXT_TYPES = "context_types";
+        private static final String KEY_PCI_ADDRESS = "pci_address";
+        private static final String KEY_RENDERER_FEATURES = "renderer_features";
+        private static final String KEY_RENDERER_USE_EGL = "renderer_use_egl";
+        private static final String KEY_RENDERER_USE_GLES = "renderer_use_gles";
+        private static final String KEY_RENDERER_USE_GLX = "renderer_use_glx";
+        private static final String KEY_RENDERER_USE_SURFACELESS = "renderer_use_surfaceless";
+        private static final String KEY_RENDERER_USE_VULKAN = "renderer_use_vulkan";
+
+        private final String backend;
+        private final String[] contextTypes;
+        private final String pciAddress;
+        private final String rendererFeatures;
+        private final boolean rendererUseEgl;
+        private final boolean rendererUseGles;
+        private final boolean rendererUseGlx;
+        private final boolean rendererUseSurfaceless;
+        private final boolean rendererUseVulkan;
+
+        private GpuConfig(
+                String backend,
+                String[] contextTypes,
+                String pciAddress,
+                String rendererFeatures,
+                boolean rendererUseEgl,
+                boolean rendererUseGles,
+                boolean rendererUseGlx,
+                boolean rendererUseSurfaceless,
+                boolean rendererUseVulkan) {
+            this.backend = backend;
+            this.contextTypes = contextTypes;
+            this.pciAddress = pciAddress;
+            this.rendererFeatures = rendererFeatures;
+            this.rendererUseEgl = rendererUseEgl;
+            this.rendererUseGles = rendererUseGles;
+            this.rendererUseGlx = rendererUseGlx;
+            this.rendererUseSurfaceless = rendererUseSurfaceless;
+            this.rendererUseVulkan = rendererUseVulkan;
+        }
+
+        /** @hide */
+        public String getBackend() {
+            return backend;
+        }
+
+        /** @hide */
+        public String[] getContextTypes() {
+            return contextTypes;
+        }
+
+        /** @hide */
+        public String getPciAddress() {
+            return pciAddress;
+        }
+
+        /** @hide */
+        public String getRendererFeatures() {
+            return rendererFeatures;
+        }
+
+        /** @hide */
+        public boolean getRendererUseEgl() {
+            return rendererUseEgl;
+        }
+
+        /** @hide */
+        public boolean getRendererUseGles() {
+            return rendererUseGles;
+        }
+
+        /** @hide */
+        public boolean getRendererUseGlx() {
+            return rendererUseGlx;
+        }
+
+        /** @hide */
+        public boolean getRendererUseSurfaceless() {
+            return rendererUseSurfaceless;
+        }
+
+        /** @hide */
+        public boolean getRendererUseVulkan() {
+            return rendererUseVulkan;
+        }
+
+        android.system.virtualizationservice.GpuConfig toParcelable() {
+            android.system.virtualizationservice.GpuConfig parcelable =
+                    new android.system.virtualizationservice.GpuConfig();
+            parcelable.backend = this.backend;
+            parcelable.contextTypes = this.contextTypes;
+            parcelable.pciAddress = this.pciAddress;
+            parcelable.rendererFeatures = this.rendererFeatures;
+            parcelable.rendererUseEgl = this.rendererUseEgl;
+            parcelable.rendererUseGles = this.rendererUseGles;
+            parcelable.rendererUseGlx = this.rendererUseGlx;
+            parcelable.rendererUseSurfaceless = this.rendererUseSurfaceless;
+            parcelable.rendererUseVulkan = this.rendererUseVulkan;
+            return parcelable;
+        }
+
+        private static GpuConfig from(PersistableBundle pb) {
+            if (pb == null) {
+                return null;
+            }
+            Builder builder = new Builder();
+            builder.setBackend(pb.getString(KEY_BACKEND));
+            builder.setContextTypes(pb.getStringArray(KEY_CONTEXT_TYPES));
+            builder.setPciAddress(pb.getString(KEY_PCI_ADDRESS));
+            builder.setRendererFeatures(pb.getString(KEY_RENDERER_FEATURES));
+            builder.setRendererUseEgl(pb.getBoolean(KEY_RENDERER_USE_EGL));
+            builder.setRendererUseGles(pb.getBoolean(KEY_RENDERER_USE_GLES));
+            builder.setRendererUseGlx(pb.getBoolean(KEY_RENDERER_USE_GLX));
+            builder.setRendererUseSurfaceless(pb.getBoolean(KEY_RENDERER_USE_SURFACELESS));
+            builder.setRendererUseVulkan(pb.getBoolean(KEY_RENDERER_USE_VULKAN));
+            return builder.build();
+        }
+
+        private PersistableBundle toPersistableBundle() {
+            PersistableBundle pb = new PersistableBundle();
+            pb.putString(KEY_BACKEND, this.backend);
+            pb.putStringArray(KEY_CONTEXT_TYPES, this.contextTypes);
+            pb.putString(KEY_PCI_ADDRESS, this.pciAddress);
+            pb.putString(KEY_RENDERER_FEATURES, this.rendererFeatures);
+            pb.putBoolean(KEY_RENDERER_USE_EGL, this.rendererUseEgl);
+            pb.putBoolean(KEY_RENDERER_USE_GLES, this.rendererUseGles);
+            pb.putBoolean(KEY_RENDERER_USE_GLX, this.rendererUseGlx);
+            pb.putBoolean(KEY_RENDERER_USE_SURFACELESS, this.rendererUseSurfaceless);
+            pb.putBoolean(KEY_RENDERER_USE_VULKAN, this.rendererUseVulkan);
+            return pb;
+        }
+
+        /** @hide */
+        public static class Builder {
+            private String backend;
+            private String[] contextTypes;
+            private String pciAddress;
+            private String rendererFeatures;
+            private boolean rendererUseEgl = true;
+            private boolean rendererUseGles = true;
+            private boolean rendererUseGlx = false;
+            private boolean rendererUseSurfaceless = true;
+            private boolean rendererUseVulkan = false;
+
+            /** @hide */
+            public Builder() {}
+
+            /** @hide */
+            public Builder setBackend(String backend) {
+                this.backend = backend;
+                return this;
+            }
+
+            /** @hide */
+            public Builder setContextTypes(String[] contextTypes) {
+                this.contextTypes = contextTypes;
+                return this;
+            }
+
+            /** @hide */
+            public Builder setPciAddress(String pciAddress) {
+                this.pciAddress = pciAddress;
+                return this;
+            }
+
+            /** @hide */
+            public Builder setRendererFeatures(String rendererFeatures) {
+                this.rendererFeatures = rendererFeatures;
+                return this;
+            }
+
+            /** @hide */
+            public Builder setRendererUseEgl(Boolean rendererUseEgl) {
+                this.rendererUseEgl = rendererUseEgl;
+                return this;
+            }
+
+            /** @hide */
+            public Builder setRendererUseGles(Boolean rendererUseGles) {
+                this.rendererUseGles = rendererUseGles;
+                return this;
+            }
+
+            /** @hide */
+            public Builder setRendererUseGlx(Boolean rendererUseGlx) {
+                this.rendererUseGlx = rendererUseGlx;
+                return this;
+            }
+
+            /** @hide */
+            public Builder setRendererUseSurfaceless(Boolean rendererUseSurfaceless) {
+                this.rendererUseSurfaceless = rendererUseSurfaceless;
+                return this;
+            }
+
+            /** @hide */
+            public Builder setRendererUseVulkan(Boolean rendererUseVulkan) {
+                this.rendererUseVulkan = rendererUseVulkan;
+                return this;
+            }
+
+            /** @hide */
+            public GpuConfig build() {
+                return new GpuConfig(
+                        backend,
+                        contextTypes,
+                        pciAddress,
+                        rendererFeatures,
+                        rendererUseEgl,
+                        rendererUseGles,
+                        rendererUseGlx,
+                        rendererUseSurfaceless,
+                        rendererUseVulkan);
+            }
+        }
+    }
 }
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineManager.java b/java/framework/src/android/system/virtualmachine/VirtualMachineManager.java
index 4a9e943..abb2c81 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachineManager.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineManager.java
@@ -123,6 +123,7 @@
                 FEATURE_DICE_CHANGES,
                 FEATURE_LLPVM_CHANGES,
                 FEATURE_MULTI_TENANT,
+                FEATURE_NETWORK,
                 FEATURE_REMOTE_ATTESTATION,
                 FEATURE_VENDOR_MODULES,
             })
@@ -147,6 +148,13 @@
     public static final String FEATURE_MULTI_TENANT = IVirtualizationService.FEATURE_MULTI_TENANT;
 
     /**
+     * Feature to allow network features in VM.
+     *
+     * @hide
+     */
+    @TestApi public static final String FEATURE_NETWORK = IVirtualizationService.FEATURE_NETWORK;
+
+    /**
      * Feature to allow remote attestation in Microdroid.
      *
      * @hide
diff --git a/java/jni/android_system_virtualmachine_VirtualMachine.cpp b/java/jni/android_system_virtualmachine_VirtualMachine.cpp
index b3354cc..ed102bf 100644
--- a/java/jni/android_system_virtualmachine_VirtualMachine.cpp
+++ b/java/jni/android_system_virtualmachine_VirtualMachine.cpp
@@ -17,16 +17,36 @@
 #define LOG_TAG "VirtualMachine"
 
 #include <aidl/android/system/virtualizationservice/IVirtualMachine.h>
+#include <android-base/scopeguard.h>
+#include <android-base/strings.h>
 #include <android/binder_auto_utils.h>
 #include <android/binder_ibinder_jni.h>
+#include <fcntl.h>
 #include <jni.h>
 #include <log/log.h>
+#include <nativehelper/JNIHelp.h>
+#include <nativehelper/JNIPlatformHelp.h>
+#include <nativehelper/ScopedLocalRef.h>
+#include <pty.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <termios.h>
+#include <unistd.h>
 
 #include <binder_rpc_unstable.hpp>
+#include <string>
 #include <tuple>
 
 #include "common.h"
 
+namespace {
+
+void throwIOException(JNIEnv *env, const std::string &msg) {
+    jniThrowException(env, "java/io/IOException", msg.c_str());
+}
+
+} // namespace
+
 extern "C" JNIEXPORT jobject JNICALL
 Java_android_system_virtualmachine_VirtualMachine_nativeConnectToVsockServer(
         JNIEnv* env, [[maybe_unused]] jclass clazz, jobject vmBinder, jint port) {
@@ -65,3 +85,72 @@
     auto client = ARpcSession_setupPreconnectedClient(session.get(), requestFunc, &args);
     return AIBinder_toJavaBinder(env, client);
 }
+
+extern "C" JNIEXPORT void JNICALL
+Java_android_system_virtualmachine_VirtualMachine_nativeOpenPtyRawNonblock(
+        JNIEnv *env, [[maybe_unused]] jclass clazz, jobject resultCallback) {
+    int pm, ps;
+    // man openpty says: "Nobody knows how much space should be reserved for name."
+    // but on modern Linux the format of the pts name is always `/dev/pts/[0-9]+`
+    // Realistically speaking, a buffer of 32 bytes leaves us with 22 digits for the pts number,
+    // which should be more than enough.
+    // NOTE: bionic implements openpty() with internal name buffer of size 32, musl 20.
+    char name[32];
+    if (openpty(&pm, &ps, name, nullptr, nullptr)) {
+        throwIOException(env, "openpty(): " + android::base::ErrnoNumberAsString(errno));
+        return;
+    }
+    fcntl(pm, F_SETFD, FD_CLOEXEC);
+    fcntl(ps, F_SETFD, FD_CLOEXEC);
+    name[sizeof(name) - 1] = '\0';
+    // Set world RW so adb shell can talk to the pts.
+    chmod(name, 0666);
+
+    if (int flags = fcntl(pm, F_GETFL, 0); flags < 0) {
+        throwIOException(env, "fcntl(F_GETFL): " + android::base::ErrnoNumberAsString(errno));
+        return;
+    } else if (fcntl(pm, F_SETFL, flags | O_NONBLOCK) < 0) {
+        throwIOException(env, "fcntl(F_SETFL): " + android::base::ErrnoNumberAsString(errno));
+        return;
+    }
+
+    android::base::ScopeGuard cleanup_handler([=] {
+        close(ps);
+        close(pm);
+    });
+
+    struct termios tio;
+    if (tcgetattr(pm, &tio)) {
+        throwIOException(env, "tcgetattr(): " + android::base::ErrnoNumberAsString(errno));
+        return;
+    }
+    cfmakeraw(&tio);
+    if (tcsetattr(pm, TCSANOW, &tio)) {
+        throwIOException(env, "tcsetattr(): " + android::base::ErrnoNumberAsString(errno));
+        return;
+    }
+
+    jobject mfd = jniCreateFileDescriptor(env, pm);
+    if (mfd == nullptr) {
+        return;
+    }
+    jobject sfd = jniCreateFileDescriptor(env, ps);
+    if (sfd == nullptr) {
+        return;
+    }
+    size_t len = strlen(name);
+    ScopedLocalRef<jbyteArray> ptsName(env, env->NewByteArray(len));
+    if (ptsName.get() != nullptr) {
+        env->SetByteArrayRegion(ptsName.get(), 0, len, (jbyte *)name);
+    }
+    ScopedLocalRef<jclass> callback_class(env, env->GetObjectClass(resultCallback));
+    jmethodID mid = env->GetMethodID(callback_class.get(), "apply",
+                                     "(Ljava/io/FileDescriptor;Ljava/io/FileDescriptor;[B)V");
+    if (mid == nullptr) {
+        return;
+    }
+
+    env->CallVoidMethod(resultCallback, mid, mfd, sfd, ptsName.get());
+    // FD ownership is transferred to the callback, reset the auto-close hander.
+    cleanup_handler.Disable();
+}
diff --git a/java/service/Android.bp b/java/service/Android.bp
index 8bac7be..814445c 100644
--- a/java/service/Android.bp
+++ b/java/service/Android.bp
@@ -31,6 +31,7 @@
     ],
     static_libs: [
         "android.system.virtualizationmaintenance-java",
+        "android.system.vmtethering-java",
     ],
     sdk_version: "core_platform",
     apex_available: ["com.android.virt"],
diff --git a/java/service/src/com/android/system/virtualmachine/VirtualizationSystemService.java b/java/service/src/com/android/system/virtualmachine/VirtualizationSystemService.java
index 2461755..241eef4 100644
--- a/java/service/src/com/android/system/virtualmachine/VirtualizationSystemService.java
+++ b/java/service/src/com/android/system/virtualmachine/VirtualizationSystemService.java
@@ -21,16 +21,23 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.net.TetheringManager;
+import android.net.TetheringManager.StartTetheringCallback;
+import android.net.TetheringManager.TetheringRequest;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.ServiceManager;
 import android.os.UserHandle;
 import android.system.virtualizationmaintenance.IVirtualizationMaintenance;
+import android.system.vmtethering.IVmTethering;
 import android.util.Log;
 
 import com.android.internal.os.BackgroundThread;
 import com.android.server.SystemService;
 
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
 /**
  * This class exists to notify virtualization service of relevant things happening in the Android
  * framework.
@@ -40,17 +47,25 @@
  */
 public class VirtualizationSystemService extends SystemService {
     private static final String TAG = VirtualizationSystemService.class.getName();
-    private static final String SERVICE_NAME = "android.system.virtualizationmaintenance";
+    private static final String MAINTENANCE_SERVICE_NAME =
+            "android.system.virtualizationmaintenance";
     private Handler mHandler;
+    private final TetheringService mTetheringService;
 
     public VirtualizationSystemService(Context context) {
         super(context);
+        if (Files.exists(Paths.get("/apex/com.android.virt/bin/vmnic"))) {
+            mTetheringService = new TetheringService();
+        } else {
+            mTetheringService = null;
+        }
     }
 
     @Override
     public void onStart() {
-        // Nothing needed here - we don't expose any binder service. The binder service we use is
-        // exposed as a lazy service by the virtualizationservice native binary.
+        if (mTetheringService != null) {
+            publishBinderService(IVmTethering.DESCRIPTOR, mTetheringService);
+        }
     }
 
     @Override
@@ -82,11 +97,11 @@
     }
 
     static IVirtualizationMaintenance connectToMaintenanceService() {
-        IBinder binder = ServiceManager.waitForService(SERVICE_NAME);
+        IBinder binder = ServiceManager.waitForService(MAINTENANCE_SERVICE_NAME);
         IVirtualizationMaintenance maintenance =
                 IVirtualizationMaintenance.Stub.asInterface(binder);
         if (maintenance == null) {
-            throw new IllegalStateException("Failed to connect to " + SERVICE_NAME);
+            throw new IllegalStateException("Failed to connect to " + MAINTENANCE_SERVICE_NAME);
         }
         return maintenance;
     }
@@ -136,4 +151,38 @@
             }
         }
     }
+
+    private final class TetheringService extends IVmTethering.Stub {
+        private final TetheringManager tm = getContext().getSystemService(TetheringManager.class);
+
+        @Override
+        public void enableVmTethering() {
+            final TetheringRequest tr =
+                    new TetheringRequest.Builder(TetheringManager.TETHERING_VIRTUAL)
+                            .setConnectivityScope(TetheringManager.CONNECTIVITY_SCOPE_GLOBAL)
+                            .build();
+
+            StartTetheringCallback startTetheringCallback =
+                    new StartTetheringCallback() {
+                        @Override
+                        public void onTetheringStarted() {
+                            Log.i(TAG, "VM tethering started successfully");
+                        }
+
+                        @Override
+                        public void onTetheringFailed(int resultCode) {
+                            Log.e(
+                                    TAG,
+                                    "VM tethering failed. Result Code: "
+                                            + Integer.toString(resultCode));
+                        }
+                    };
+            tm.startTethering(tr, c -> c.run() /* executor */, startTetheringCallback);
+        }
+
+        @Override
+        public void disableVmTethering() {
+            tm.stopTethering(TetheringManager.TETHERING_VIRTUAL);
+        }
+    }
 }
diff --git a/libs/android_display_backend/aidl/android/crosvm/ICrosvmAndroidDisplayService.aidl b/libs/android_display_backend/aidl/android/crosvm/ICrosvmAndroidDisplayService.aidl
index c7bfc80..e42cdd1 100644
--- a/libs/android_display_backend/aidl/android/crosvm/ICrosvmAndroidDisplayService.aidl
+++ b/libs/android_display_backend/aidl/android/crosvm/ICrosvmAndroidDisplayService.aidl
@@ -16,6 +16,7 @@
 
 package android.crosvm;
 
+import android.os.ParcelFileDescriptor;
 import android.view.Surface;
 
 /**
@@ -23,7 +24,7 @@
  * display.
  */
 interface ICrosvmAndroidDisplayService {
-    void setSurface(inout Surface surface);
-
-    void removeSurface();
+    void setSurface(inout Surface surface, boolean forCursor);
+    void setCursorStream(in ParcelFileDescriptor stream);
+    void removeSurface(boolean forCursor);
 }
diff --git a/libs/android_display_backend/crosvm_android_display_client.cpp b/libs/android_display_backend/crosvm_android_display_client.cpp
index 66320f3..6e4a793 100644
--- a/libs/android_display_backend/crosvm_android_display_client.cpp
+++ b/libs/android_display_backend/crosvm_android_display_client.cpp
@@ -30,39 +30,189 @@
 
 namespace {
 
+class SinkANativeWindow_Buffer {
+public:
+    SinkANativeWindow_Buffer() = default;
+    virtual ~SinkANativeWindow_Buffer() = default;
+
+    bool configure(uint32_t width, uint32_t height, int format) {
+        if (format != HAL_PIXEL_FORMAT_BGRA_8888) {
+            return false;
+        }
+
+        mBufferBits.resize(width * height * 4);
+        mBuffer = ANativeWindow_Buffer{
+                .width = static_cast<int32_t>(width),
+                .height = static_cast<int32_t>(height),
+                .stride = static_cast<int32_t>(width),
+                .format = format,
+                .bits = mBufferBits.data(),
+        };
+        return true;
+    }
+
+    operator ANativeWindow_Buffer&() { return mBuffer; }
+
+private:
+    ANativeWindow_Buffer mBuffer;
+    std::vector<uint8_t> mBufferBits;
+};
+
+// Wrapper which contains the latest available Surface/ANativeWindow
+// from the DisplayService, if available. A Surface/ANativeWindow may
+// not always be available if, for example, the VmLauncherApp on the
+// other end of the DisplayService is not in the foreground / is paused.
+class AndroidDisplaySurface {
+public:
+    AndroidDisplaySurface() = default;
+    virtual ~AndroidDisplaySurface() = default;
+
+    void setSurface(Surface* surface) {
+        {
+            std::lock_guard lk(mSurfaceMutex);
+            mNativeSurface = std::make_unique<Surface>(surface->release());
+            mNativeSurfaceNeedsConfiguring = true;
+        }
+
+        mNativeSurfaceReady.notify_one();
+    }
+
+    void removeSurface() {
+        {
+            std::lock_guard lk(mSurfaceMutex);
+            mNativeSurface = nullptr;
+        }
+        mNativeSurfaceReady.notify_one();
+    }
+
+    Surface* getSurface() {
+        std::unique_lock lk(mSurfaceMutex);
+        return mNativeSurface.get();
+    }
+
+    void configure(uint32_t width, uint32_t height) {
+        std::unique_lock lk(mSurfaceMutex);
+
+        mRequestedSurfaceDimensions = Rect{
+                .width = width,
+                .height = height,
+        };
+
+        mSinkBuffer.configure(width, height, kFormat);
+    }
+
+    void waitForNativeSurface() {
+        std::unique_lock lk(mSurfaceMutex);
+        mNativeSurfaceReady.wait(lk, [this] { return mNativeSurface != nullptr; });
+    }
+
+    int lock(ANativeWindow_Buffer* out_buffer) {
+        std::unique_lock lk(mSurfaceMutex);
+
+        Surface* surface = mNativeSurface.get();
+        if (surface == nullptr) {
+            // Surface not currently available but not necessarily an error
+            // if, for example, the VmLauncherApp is not in the foreground.
+            *out_buffer = mSinkBuffer;
+            return 0;
+        }
+
+        ANativeWindow* anw = surface->get();
+        if (anw == nullptr) {
+            return -1;
+        }
+
+        if (mNativeSurfaceNeedsConfiguring) {
+            if (!mRequestedSurfaceDimensions) {
+                return -1;
+            }
+            const auto& dims = *mRequestedSurfaceDimensions;
+
+            // Ensure locked buffers have our desired format.
+            if (ANativeWindow_setBuffersGeometry(anw, dims.width, dims.height, kFormat) != 0) {
+                return -1;
+            }
+
+            mNativeSurfaceNeedsConfiguring = false;
+        }
+
+        return ANativeWindow_lock(anw, out_buffer, nullptr);
+    }
+
+    int unlockAndPost() {
+        std::unique_lock lk(mSurfaceMutex);
+
+        Surface* surface = mNativeSurface.get();
+        if (surface == nullptr) {
+            // Surface not currently available but not necessarily an error
+            // if, for example, the VmLauncherApp is not in the foreground.
+            return 0;
+        }
+
+        ANativeWindow* anw = surface->get();
+        if (anw == nullptr) {
+            return -1;
+        }
+
+        return ANativeWindow_unlockAndPost(anw);
+    }
+
+private:
+    // Note: crosvm always uses BGRA8888 or BGRX8888. See devices/src/virtio/gpu/mod.rs in
+    // crosvm where the SetScanoutBlob command is handled. Let's use BGRA not BGRX with a hope
+    // that we will need alpha blending for the cursor surface.
+    static constexpr const int kFormat = HAL_PIXEL_FORMAT_BGRA_8888;
+
+    std::mutex mSurfaceMutex;
+    std::unique_ptr<Surface> mNativeSurface;
+    std::condition_variable mNativeSurfaceReady;
+    bool mNativeSurfaceNeedsConfiguring = true;
+
+    SinkANativeWindow_Buffer mSinkBuffer;
+
+    struct Rect {
+        uint32_t width = 0;
+        uint32_t height = 0;
+    };
+    std::optional<Rect> mRequestedSurfaceDimensions;
+};
+
 class DisplayService : public BnCrosvmAndroidDisplayService {
 public:
     DisplayService() = default;
     virtual ~DisplayService() = default;
 
-    ndk::ScopedAStatus setSurface(Surface* surface) override {
-        {
-            std::lock_guard lk(mSurfaceReadyMutex);
-            mSurface = std::make_unique<Surface>(surface->release());
+    ndk::ScopedAStatus setSurface(Surface* surface, bool forCursor) override {
+        if (forCursor) {
+            mCursor.setSurface(surface);
+        } else {
+            mScanout.setSurface(surface);
         }
-        mSurfaceReady.notify_one();
         return ::ndk::ScopedAStatus::ok();
     }
 
-    ndk::ScopedAStatus removeSurface() override {
-        {
-            std::lock_guard lk(mSurfaceReadyMutex);
-            mSurface = nullptr;
+    ndk::ScopedAStatus removeSurface(bool forCursor) override {
+        if (forCursor) {
+            mCursor.removeSurface();
+        } else {
+            mScanout.removeSurface();
         }
-        mSurfaceReady.notify_one();
         return ::ndk::ScopedAStatus::ok();
     }
 
-    Surface* getSurface() {
-        std::unique_lock lk(mSurfaceReadyMutex);
-        mSurfaceReady.wait(lk, [this] { return mSurface != nullptr; });
-        return mSurface.get();
+    AndroidDisplaySurface* getCursorSurface() { return &mCursor; }
+    AndroidDisplaySurface* getScanoutSurface() { return &mScanout; }
+
+    ndk::ScopedFileDescriptor& getCursorStream() { return mCursorStream; }
+    ndk::ScopedAStatus setCursorStream(const ndk::ScopedFileDescriptor& in_stream) {
+        mCursorStream = ndk::ScopedFileDescriptor(dup(in_stream.get()));
+        return ::ndk::ScopedAStatus::ok();
     }
 
 private:
-    std::condition_variable mSurfaceReady;
-    std::mutex mSurfaceReadyMutex;
-    std::unique_ptr<Surface> mSurface;
+    AndroidDisplaySurface mScanout;
+    AndroidDisplaySurface mCursor;
+    ndk::ScopedFileDescriptor mCursorStream;
 };
 
 } // namespace
@@ -129,25 +279,29 @@
     delete ctx;
 }
 
-extern "C" ANativeWindow* create_android_surface(struct AndroidDisplayContext* ctx, uint32_t width,
-                                                 uint32_t height) {
+extern "C" AndroidDisplaySurface* create_android_surface(struct AndroidDisplayContext* ctx,
+                                                         uint32_t width, uint32_t height,
+                                                         bool forCursor) {
     if (ctx->disp_service == nullptr) {
         ctx->errorf("Display service was not created");
         return nullptr;
     }
-    // Note: crosvm always uses BGRA8888 or BGRX8888. See devices/src/virtio/gpu/mod.rs in crosvm
-    // where the SetScanoutBlob command is handled. Let's use BGRA not BGRX with a hope that we will
-    // need alpha blending for the cursor surface.
-    int format = HAL_PIXEL_FORMAT_BGRA_8888;
-    ANativeWindow* surface = ctx->disp_service->getSurface()->get(); // this can block
-    if (ANativeWindow_setBuffersGeometry(surface, width, height, format) != 0) {
-        ctx->errorf("Failed to set buffer gemoetry");
+
+    AndroidDisplaySurface* displaySurface = forCursor ? ctx->disp_service->getCursorSurface()
+                                                      : ctx->disp_service->getScanoutSurface();
+    if (displaySurface == nullptr) {
+        ctx->errorf("AndroidDisplaySurface was not created");
         return nullptr;
     }
+
+    displaySurface->configure(width, height);
+
+    displaySurface->waitForNativeSurface(); // this can block
+
     // TODO(b/332785161): if we know that surface can get destroyed dynamically while VM is running,
     // consider calling ANativeWindow_acquire here and _release in destroy_android_surface, so that
     // crosvm doesn't hold a dangling pointer.
-    return surface;
+    return displaySurface;
 }
 
 extern "C" void destroy_android_surface(struct AndroidDisplayContext*, ANativeWindow*) {
@@ -155,23 +309,50 @@
 }
 
 extern "C" bool get_android_surface_buffer(struct AndroidDisplayContext* ctx,
-                                           ANativeWindow* surface,
+                                           AndroidDisplaySurface* surface,
                                            ANativeWindow_Buffer* out_buffer) {
     if (out_buffer == nullptr) {
         ctx->errorf("out_buffer is null");
         return false;
     }
-    if (ANativeWindow_lock(surface, out_buffer, nullptr) != 0) {
+
+    if (surface == nullptr) {
+        ctx->errorf("Invalid AndroidDisplaySurface provided");
+        return false;
+    }
+
+    if (surface->lock(out_buffer) != 0) {
         ctx->errorf("Failed to lock buffer");
         return false;
     }
+
     return true;
 }
 
+extern "C" void set_android_surface_position(struct AndroidDisplayContext* ctx, uint32_t x,
+                                             uint32_t y) {
+    if (ctx->disp_service == nullptr) {
+        ctx->errorf("Display service was not created");
+        return;
+    }
+    auto fd = ctx->disp_service->getCursorStream().get();
+    if (fd == -1) {
+        ctx->errorf("Invalid fd");
+        return;
+    }
+    uint32_t pos[] = {x, y};
+    write(fd, pos, sizeof(pos));
+}
+
 extern "C" void post_android_surface_buffer(struct AndroidDisplayContext* ctx,
-                                            ANativeWindow* surface) {
-    if (ANativeWindow_unlockAndPost(surface) != 0) {
-        ctx->errorf("Failed to unlock and post surface.");
+                                            AndroidDisplaySurface* surface) {
+    if (surface == nullptr) {
+        ctx->errorf("Invalid AndroidDisplaySurface provided");
+        return;
+    }
+
+    if (surface->unlockAndPost() != 0) {
+        ctx->errorf("Failed to unlock and post AndroidDisplaySurface.");
         return;
     }
 }
diff --git a/libs/avf_features/src/lib.rs b/libs/avf_features/src/lib.rs
index c0faab0..1ebe2a4 100644
--- a/libs/avf_features/src/lib.rs
+++ b/libs/avf_features/src/lib.rs
@@ -16,7 +16,7 @@
 
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
     IVirtualizationService::FEATURE_DICE_CHANGES, IVirtualizationService::FEATURE_LLPVM_CHANGES,
-    IVirtualizationService::FEATURE_MULTI_TENANT,
+    IVirtualizationService::FEATURE_MULTI_TENANT, IVirtualizationService::FEATURE_NETWORK,
     IVirtualizationService::FEATURE_REMOTE_ATTESTATION,
     IVirtualizationService::FEATURE_VENDOR_MODULES,
 };
@@ -28,6 +28,7 @@
         FEATURE_DICE_CHANGES => cfg!(dice_changes),
         FEATURE_LLPVM_CHANGES => cfg!(llpvm_changes),
         FEATURE_MULTI_TENANT => cfg!(multi_tenant),
+        FEATURE_NETWORK => cfg!(network),
         FEATURE_REMOTE_ATTESTATION => cfg!(remote_attestation),
         FEATURE_VENDOR_MODULES => cfg!(vendor_modules),
         _ => {
diff --git a/libs/cborutil/src/lib.rs b/libs/cborutil/src/lib.rs
index 4d308c1..b218c82 100644
--- a/libs/cborutil/src/lib.rs
+++ b/libs/cborutil/src/lib.rs
@@ -21,7 +21,10 @@
 use alloc::string::String;
 use alloc::vec::Vec;
 use ciborium::value::{Integer, Value};
-use coset::{CborSerializable, CoseError, CoseKey, Label, Result};
+use coset::{
+    iana::{self, EnumI64},
+    CborSerializable, CoseError, CoseKey, Label, Result,
+};
 use log::error;
 use serde::{de::DeserializeOwned, Serialize};
 
@@ -132,3 +135,19 @@
         .ok_or(CoseError::UnexpectedItem("", "Label not found in CoseKey"))?
         .1)
 }
+
+/// Converts the provided COSE key algorithm integer to an `iana::Algorithm` used
+/// by DICE chains.
+pub fn dice_cose_key_alg(cose_key_alg: i32) -> Result<iana::Algorithm> {
+    let key_alg = iana::Algorithm::from_i64(cose_key_alg as i64).ok_or_else(|| {
+        error!("Unsupported COSE key algorithm for DICE: {cose_key_alg}");
+        CoseError::UnexpectedItem("COSE key algorithm", "")
+    })?;
+    match key_alg {
+        iana::Algorithm::EdDSA | iana::Algorithm::ES256 | iana::Algorithm::ES384 => Ok(key_alg),
+        _ => {
+            error!("Unsupported COSE key algorithm for DICE: {key_alg:?}");
+            Err(CoseError::UnexpectedItem("-8, -7 or -35", ""))
+        }
+    }
+}
diff --git a/libs/dice/open_dice/Android.bp b/libs/dice/open_dice/Android.bp
index ab3220e..4904672 100644
--- a/libs/dice/open_dice/Android.bp
+++ b/libs/dice/open_dice/Android.bp
@@ -161,6 +161,7 @@
         "--allowlist-var=DICE_PUBLIC_KEY_SIZE",
         "--allowlist-var=DICE_PRIVATE_KEY_SIZE",
         "--allowlist-var=DICE_SIGNATURE_SIZE",
+        "--allowlist-var=DICE_COSE_KEY_ALG_VALUE",
     ],
 }
 
diff --git a/libs/dice/open_dice/src/lib.rs b/libs/dice/open_dice/src/lib.rs
index d0004b1..085a2cd 100644
--- a/libs/dice/open_dice/src/lib.rs
+++ b/libs/dice/open_dice/src/lib.rs
@@ -40,6 +40,10 @@
     PublicKey, Signature, CDI_SIZE, HASH_SIZE, HIDDEN_SIZE, ID_SIZE, PRIVATE_KEY_SEED_SIZE,
 };
 pub use error::{DiceError, Result};
+// Currently, open-dice library only supports a single signing and verification algorithm.
+// The value of DICE_COSE_KEY_ALG_VALUE depends on the algorithm chosen by the underlying C
+// library at build time. Refer to b/342333212 for more information.
+pub use open_dice_cbor_bindgen::DICE_COSE_KEY_ALG_VALUE;
 pub use ops::{
     derive_cdi_leaf_priv, generate_certificate, hash, kdf, keypair_from_seed, sign, verify,
 };
diff --git a/libs/hypervisor_props/src/lib.rs b/libs/hypervisor_props/src/lib.rs
index 14614fd..6665bc5 100644
--- a/libs/hypervisor_props/src/lib.rs
+++ b/libs/hypervisor_props/src/lib.rs
@@ -37,3 +37,8 @@
 pub fn version() -> Result<Option<String>> {
     Ok(hypervisorproperties::hypervisor_version()?)
 }
+
+/// Returns if the hypervisor is pKVM
+pub fn is_pkvm() -> Result<bool> {
+    Ok(version()?.unwrap_or_default().starts_with("kvm") && is_protected_vm_supported()?)
+}
diff --git a/libs/vmconfig/src/lib.rs b/libs/vmconfig/src/lib.rs
index 7c917b0..1413b51 100644
--- a/libs/vmconfig/src/lib.rs
+++ b/libs/vmconfig/src/lib.rs
@@ -65,6 +65,8 @@
     /// SysFS paths of devices assigned to the VM.
     #[serde(default)]
     pub devices: Vec<PathBuf>,
+    /// The serial device for VM console input.
+    pub console_input_device: Option<String>,
 }
 
 impl VmConfig {
@@ -124,6 +126,7 @@
                     x.to_str().map(String::from).ok_or(anyhow!("Failed to convert {x:?} to String"))
                 })
                 .collect::<Result<_>>()?,
+            consoleInputDevice: self.console_input_device.clone(),
             ..Default::default()
         })
     }
diff --git a/microdroid/microdroid.json b/microdroid/microdroid.json
index 00cedc8..e60c4ca 100644
--- a/microdroid/microdroid.json
+++ b/microdroid/microdroid.json
@@ -16,5 +16,6 @@
     }
   ],
   "memory_mib": 256,
+  "console_input_device": "hvc0",
   "platform_version": "~1.0"
 }
diff --git a/microdroid_manager/src/verify.rs b/microdroid_manager/src/verify.rs
index 65c32b0..84feb68 100644
--- a/microdroid_manager/src/verify.rs
+++ b/microdroid_manager/src/verify.rs
@@ -14,7 +14,7 @@
 
 use crate::instance::{ApexData, ApkData, MicrodroidData};
 use crate::payload::{get_apex_data_from_payload, to_metadata};
-use crate::{is_strict_boot, is_verified_boot, MicrodroidError};
+use crate::{is_strict_boot, MicrodroidError};
 use anyhow::{anyhow, ensure, Context, Result};
 use apkmanifest::get_manifest_info;
 use apkverify::{extract_signed_data, verify, V4Signature};
@@ -130,11 +130,10 @@
     // APEX payload.
     let apex_data_from_payload = get_apex_data_from_payload(metadata)?;
 
-    // Writing /apex/vm-payload-metadata is to verify that the payload isn't changed.
-    // Skip writing it if the debug policy ignoring identity is on
-    if is_verified_boot() {
-        write_apex_payload_data(saved_data, &apex_data_from_payload)?;
-    }
+    // To prevent a TOCTOU attack, we need to make sure that when apexd verifies & mounts the
+    // APEXes it sees the same ones that we just read - so we write the metadata we just collected
+    // to a file (that the host can't access) that apexd will then verify against. See b/199371341.
+    write_apex_payload_data(saved_data, &apex_data_from_payload)?;
 
     if cfg!(not(dice_changes)) {
         // Start apexd to activate APEXes
@@ -222,16 +221,17 @@
             saved_apex_data == apex_data_from_payload,
             MicrodroidError::PayloadChanged(String::from("APEXes have changed."))
         );
-        let apex_metadata = to_metadata(apex_data_from_payload);
-        // Pass metadata(with public keys and root digests) to apexd so that it uses the passed
-        // metadata instead of the default one (/dev/block/by-name/payload-metadata)
-        OpenOptions::new()
-            .create_new(true)
-            .write(true)
-            .open("/apex/vm-payload-metadata")
-            .context("Failed to open /apex/vm-payload-metadata")
-            .and_then(|f| write_metadata(&apex_metadata, f))?;
     }
+    let apex_metadata = to_metadata(apex_data_from_payload);
+    // Pass metadata(with public keys and root digests) to apexd so that it uses the passed
+    // metadata instead of the default one (/dev/block/by-name/payload-metadata)
+    OpenOptions::new()
+        .create_new(true)
+        .write(true)
+        .open("/apex/vm-payload-metadata")
+        .context("Failed to open /apex/vm-payload-metadata")
+        .and_then(|f| write_metadata(&apex_metadata, f))?;
+
     Ok(())
 }
 
diff --git a/microdroid_manager/src/vm_secret.rs b/microdroid_manager/src/vm_secret.rs
index ec40b45..c16a45e 100644
--- a/microdroid_manager/src/vm_secret.rs
+++ b/microdroid_manager/src/vm_secret.rs
@@ -20,7 +20,7 @@
 use secretkeeper_comm::data_types::request::Request;
 use binder::{Strong};
 use coset::{CoseKey, CborSerializable, CborOrdering};
-use dice_policy_builder::{CertIndex, ConstraintSpec, ConstraintType, policy_for_dice_chain, MissingAction, WILDCARD_FULL_ARRAY};
+use dice_policy_builder::{TargetEntry, ConstraintSpec, ConstraintType, policy_for_dice_chain, MissingAction, WILDCARD_FULL_ARRAY};
 use diced_open_dice::{DiceArtifacts, OwnedDiceArtifacts};
 use keystore2_crypto::ZVec;
 use openssl::hkdf::hkdf;
@@ -45,9 +45,10 @@
 const SUBCOMPONENT_DESCRIPTORS: i64 = -71002;
 const SUBCOMPONENT_SECURITY_VERSION: i64 = 2;
 const SUBCOMPONENT_AUTHORITY_HASH: i64 = 4;
-// Index of DiceChainEntry corresponding to Payload (relative to the end considering DICE Chain
-// as an array)
-const PAYLOAD_INDEX_FROM_END: usize = 0;
+// See dice_for_avf_guest.cddl for the `component_name` used by different boot stages in guest VM.
+const MICRODROID_PAYLOAD_COMPONENT_NAME: &str = "Microdroid payload";
+const GUEST_OS_COMPONENT_NAME: &str = "vm_entry";
+const INSTANCE_HASH_KEY: i64 = -71003;
 
 // Generated using hexdump -vn32 -e'14/1 "0x%02X, " 1 "\n"' /dev/urandom
 const SALT_ENCRYPTED_STORE: &[u8] = &[
@@ -173,25 +174,27 @@
 //     microdroid_manager/src/vm_config.cddl):
 //       - GreaterOrEqual on SECURITY_VERSION (Required)
 //       - ExactMatch on AUTHORITY_HASH (Required).
+//  5. ExactMatch on Instance Hash (Required) - This uniquely identifies one VM instance from
+//     another even if they are running the exact same images.
 fn sealing_policy(dice: &[u8]) -> Result<Vec<u8>, String> {
-    let constraint_spec = [
+    let constraint_spec = vec![
         ConstraintSpec::new(
             ConstraintType::ExactMatch,
             vec![AUTHORITY_HASH],
             MissingAction::Fail,
-            CertIndex::All,
+            TargetEntry::All,
         ),
         ConstraintSpec::new(
             ConstraintType::ExactMatch,
             vec![MODE],
             MissingAction::Fail,
-            CertIndex::All,
+            TargetEntry::All,
         ),
         ConstraintSpec::new(
             ConstraintType::GreaterOrEqual,
             vec![CONFIG_DESC, SECURITY_VERSION],
             MissingAction::Ignore,
-            CertIndex::All,
+            TargetEntry::All,
         ),
         ConstraintSpec::new(
             ConstraintType::GreaterOrEqual,
@@ -202,7 +205,7 @@
                 SUBCOMPONENT_SECURITY_VERSION,
             ],
             MissingAction::Fail,
-            CertIndex::FromEnd(PAYLOAD_INDEX_FROM_END),
+            TargetEntry::ByName(MICRODROID_PAYLOAD_COMPONENT_NAME.to_string()),
         ),
         ConstraintSpec::new(
             ConstraintType::ExactMatch,
@@ -213,11 +216,17 @@
                 SUBCOMPONENT_AUTHORITY_HASH,
             ],
             MissingAction::Fail,
-            CertIndex::FromEnd(PAYLOAD_INDEX_FROM_END),
+            TargetEntry::ByName(MICRODROID_PAYLOAD_COMPONENT_NAME.to_string()),
+        ),
+        ConstraintSpec::new(
+            ConstraintType::ExactMatch,
+            vec![CONFIG_DESC, INSTANCE_HASH_KEY],
+            MissingAction::Fail,
+            TargetEntry::ByName(GUEST_OS_COMPONENT_NAME.to_string()),
         ),
     ];
 
-    policy_for_dice_chain(dice, &constraint_spec)?
+    policy_for_dice_chain(dice, constraint_spec)?
         .to_vec()
         .map_err(|e| format!("DicePolicy construction failed {e:?}"))
 }
diff --git a/pvmfw/Android.bp b/pvmfw/Android.bp
index 7881c7c..144e81e 100644
--- a/pvmfw/Android.bp
+++ b/pvmfw/Android.bp
@@ -14,6 +14,7 @@
         "libaarch64_paging",
         "libbssl_avf_nostd",
         "libbssl_sys_nostd",
+        "libcbor_util_nostd",
         "libciborium_nostd",
         "libciborium_io_nostd",
         "libcstr",
@@ -116,9 +117,10 @@
     rustlibs: [
         "libcbor_util",
         "libciborium",
-        "libdiced_open_dice_nostd",
+        "libdiced_open_dice",
         "libpvmfw_avb_nostd",
         "libzerocopy_nostd",
+        "libhex",
     ],
 }
 
diff --git a/pvmfw/platform.dts b/pvmfw/platform.dts
index 68acf13..2df0768 100644
--- a/pvmfw/platform.dts
+++ b/pvmfw/platform.dts
@@ -308,11 +308,11 @@
 			      GIC_PPI 0xa IRQ_TYPE_LEVEL_LOW>;
 	};
 
-	uart@2e8 {
+	uart@3f8 {
 		compatible = "ns16550a";
-		reg = <0x00 0x2e8 0x00 0x8>;
+		reg = <0x00 0x3f8 0x00 0x8>;
 		clock-frequency = <0x1c2000>;
-		interrupts = <GIC_SPI 2 IRQ_TYPE_EDGE_RISING>;
+		interrupts = <GIC_SPI 0 IRQ_TYPE_EDGE_RISING>;
 	};
 
 	uart@2f8 {
@@ -329,11 +329,11 @@
 		interrupts = <GIC_SPI 0 IRQ_TYPE_EDGE_RISING>;
 	};
 
-	uart@3f8 {
+	uart@2e8 {
 		compatible = "ns16550a";
-		reg = <0x00 0x3f8 0x00 0x8>;
+		reg = <0x00 0x2e8 0x00 0x8>;
 		clock-frequency = <0x1c2000>;
-		interrupts = <GIC_SPI 0 IRQ_TYPE_EDGE_RISING>;
+		interrupts = <GIC_SPI 2 IRQ_TYPE_EDGE_RISING>;
 	};
 
 	psci {
@@ -366,6 +366,12 @@
 			0x4000 0x0 0x0 1 &intc 0 0 GIC_SPI (IRQ_BASE + 7) IRQ_TYPE_LEVEL_HIGH
 			0x4800 0x0 0x0 1 &intc 0 0 GIC_SPI (IRQ_BASE + 8) IRQ_TYPE_LEVEL_HIGH
 			0x5000 0x0 0x0 1 &intc 0 0 GIC_SPI (IRQ_BASE + 9) IRQ_TYPE_LEVEL_HIGH
+			0x5800 0x0 0x0 1 &intc 0 0 GIC_SPI (IRQ_BASE + 10) IRQ_TYPE_LEVEL_HIGH
+			0x6000 0x0 0x0 1 &intc 0 0 GIC_SPI (IRQ_BASE + 11) IRQ_TYPE_LEVEL_HIGH
+			0x6800 0x0 0x0 1 &intc 0 0 GIC_SPI (IRQ_BASE + 12) IRQ_TYPE_LEVEL_HIGH
+			0x7000 0x0 0x0 1 &intc 0 0 GIC_SPI (IRQ_BASE + 13) IRQ_TYPE_LEVEL_HIGH
+			0x7800 0x0 0x0 1 &intc 0 0 GIC_SPI (IRQ_BASE + 14) IRQ_TYPE_LEVEL_HIGH
+			0x8000 0x0 0x0 1 &intc 0 0 GIC_SPI (IRQ_BASE + 15) IRQ_TYPE_LEVEL_HIGH
 		>;
 		interrupt-map-mask = <0xf800 0x0 0x0 0x7
 				      0xf800 0x0 0x0 0x7
@@ -376,6 +382,12 @@
 				      0xf800 0x0 0x0 0x7
 				      0xf800 0x0 0x0 0x7
 				      0xf800 0x0 0x0 0x7
+				      0xf800 0x0 0x0 0x7
+				      0xf800 0x0 0x0 0x7
+				      0xf800 0x0 0x0 0x7
+				      0xf800 0x0 0x0 0x7
+				      0xf800 0x0 0x0 0x7
+				      0xf800 0x0 0x0 0x7
 				      0xf800 0x0 0x0 0x7>;
 	};
 
diff --git a/pvmfw/src/bcc.rs b/pvmfw/src/bcc.rs
index f56e62b..7a13da7 100644
--- a/pvmfw/src/bcc.rs
+++ b/pvmfw/src/bcc.rs
@@ -27,10 +27,9 @@
 type Result<T> = core::result::Result<T, BccError>;
 
 pub enum BccError {
-    CborDecodeError(ciborium::de::Error<ciborium_io::EndOfFile>),
-    CborEncodeError(ciborium::ser::Error<core::convert::Infallible>),
+    CborDecodeError,
+    CborEncodeError,
     DiceError(diced_open_dice::DiceError),
-    ExtraneousBytes,
     MalformedBcc(&'static str),
     MissingBcc,
 }
@@ -38,10 +37,9 @@
 impl fmt::Display for BccError {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         match self {
-            Self::CborDecodeError(e) => write!(f, "Error parsing BCC CBOR: {e:?}"),
-            Self::CborEncodeError(e) => write!(f, "Error encoding BCC CBOR: {e:?}"),
+            Self::CborDecodeError => write!(f, "Error parsing BCC CBOR"),
+            Self::CborEncodeError => write!(f, "Error encoding BCC CBOR"),
             Self::DiceError(e) => write!(f, "Dice error: {e:?}"),
-            Self::ExtraneousBytes => write!(f, "Unexpected trailing data in BCC"),
             Self::MalformedBcc(s) => {
                 write!(f, "BCC does not have the expected CBOR structure: {s}")
             }
@@ -65,7 +63,7 @@
     // }
     let bcc_handover: Vec<(Value, Value)> =
         vec![(1.into(), cdi_attest.as_slice().into()), (2.into(), cdi_seal.as_slice().into())];
-    value_to_bytes(&bcc_handover.into())
+    cbor_util::serialize(&bcc_handover).map_err(|_| BccError::CborEncodeError)
 }
 
 fn taint_cdi(cdi: &Cdi, info: &str) -> Result<Cdi> {
@@ -100,7 +98,8 @@
         // We don't attempt to fully validate the BCC (e.g. we don't check the signatures) - we
         // have to trust our loader. But if it's invalid CBOR or otherwise clearly ill-formed,
         // something is very wrong, so we fail.
-        let bcc_cbor = value_from_bytes(received_bcc)?;
+        let bcc_cbor =
+            cbor_util::deserialize(received_bcc).map_err(|_| BccError::CborDecodeError)?;
 
         // Bcc = [
         //   PubKeyEd25519 / PubKeyECDSA256, // DK_pub
@@ -159,7 +158,7 @@
         // ]
         let payload =
             self.payload_bytes().ok_or(BccError::MalformedBcc("Invalid payload in BccEntry"))?;
-        let payload = value_from_bytes(payload)?;
+        let payload = cbor_util::deserialize(payload).map_err(|_| BccError::CborDecodeError)?;
         trace!("Bcc payload: {payload:?}");
         Ok(BccPayload(payload))
     }
@@ -215,21 +214,3 @@
         None
     }
 }
-
-/// Decodes the provided binary CBOR-encoded value and returns a
-/// ciborium::Value struct wrapped in Result.
-fn value_from_bytes(mut bytes: &[u8]) -> Result<Value> {
-    let value = ciborium::de::from_reader(&mut bytes).map_err(BccError::CborDecodeError)?;
-    // Ciborium tries to read one Value, but doesn't care if there is trailing data after it. We do.
-    if !bytes.is_empty() {
-        return Err(BccError::ExtraneousBytes);
-    }
-    Ok(value)
-}
-
-/// Encodes a ciborium::Value into bytes.
-fn value_to_bytes(value: &Value) -> Result<Vec<u8>> {
-    let mut bytes: Vec<u8> = Vec::new();
-    ciborium::ser::into_writer(&value, &mut bytes).map_err(BccError::CborEncodeError)?;
-    Ok(bytes)
-}
diff --git a/pvmfw/src/dice.rs b/pvmfw/src/dice.rs
index 67865e5..da19931 100644
--- a/pvmfw/src/dice.rs
+++ b/pvmfw/src/dice.rs
@@ -13,16 +13,48 @@
 // limitations under the License.
 
 //! Support for DICE derivation and BCC generation.
+extern crate alloc;
 
+use alloc::format;
+use alloc::vec::Vec;
+use ciborium::cbor;
+use ciborium::Value;
 use core::mem::size_of;
-use cstr::cstr;
 use diced_open_dice::{
-    bcc_format_config_descriptor, bcc_handover_main_flow, hash, Config, DiceConfigValues, DiceMode,
-    Hash, InputValues, HIDDEN_SIZE,
+    bcc_handover_main_flow, hash, Config, DiceMode, Hash, InputValues, HIDDEN_SIZE,
 };
 use pvmfw_avb::{Capability, DebugLevel, Digest, VerifiedBootData};
 use zerocopy::AsBytes;
 
+const COMPONENT_NAME_KEY: i64 = -70002;
+const SECURITY_VERSION_KEY: i64 = -70005;
+const RKP_VM_MARKER_KEY: i64 = -70006;
+// TODO(b/291245237): Document this key along with others used in ConfigDescriptor in AVF based VM.
+const INSTANCE_HASH_KEY: i64 = -71003;
+
+#[derive(Debug)]
+pub enum Error {
+    /// Error in CBOR operations
+    CborError(ciborium::value::Error),
+    /// Error in DICE operations
+    DiceError(diced_open_dice::DiceError),
+}
+
+impl From<ciborium::value::Error> for Error {
+    fn from(e: ciborium::value::Error) -> Self {
+        Self::CborError(e)
+    }
+}
+
+impl From<diced_open_dice::DiceError> for Error {
+    fn from(e: diced_open_dice::DiceError) -> Self {
+        Self::DiceError(e)
+    }
+}
+
+// DICE in pvmfw result type.
+type Result<T> = core::result::Result<T, Error>;
+
 fn to_dice_mode(debug_level: DebugLevel) -> DiceMode {
     match debug_level {
         DebugLevel::None => DiceMode::kDiceModeNormal,
@@ -30,15 +62,16 @@
     }
 }
 
-fn to_dice_hash(verified_boot_data: &VerifiedBootData) -> diced_open_dice::Result<Hash> {
+fn to_dice_hash(verified_boot_data: &VerifiedBootData) -> Result<Hash> {
     let mut digests = [0u8; size_of::<Digest>() * 2];
     digests[..size_of::<Digest>()].copy_from_slice(&verified_boot_data.kernel_digest);
     if let Some(initrd_digest) = verified_boot_data.initrd_digest {
         digests[size_of::<Digest>()..].copy_from_slice(&initrd_digest);
     }
-    hash(&digests)
+    Ok(hash(&digests)?)
 }
 
+#[derive(Clone)]
 pub struct PartialInputs {
     pub code_hash: Hash,
     pub auth_hash: Hash,
@@ -48,7 +81,7 @@
 }
 
 impl PartialInputs {
-    pub fn new(data: &VerifiedBootData) -> diced_open_dice::Result<Self> {
+    pub fn new(data: &VerifiedBootData) -> Result<Self> {
         let code_hash = to_dice_hash(data)?;
         let auth_hash = hash(data.public_key)?;
         let mode = to_dice_mode(data.debug_level);
@@ -63,26 +96,36 @@
         self,
         current_bcc_handover: &[u8],
         salt: &[u8; HIDDEN_SIZE],
+        instance_hash: Option<Hash>,
+        deferred_rollback_protection: bool,
         next_bcc: &mut [u8],
-    ) -> diced_open_dice::Result<()> {
-        let mut config_descriptor_buffer = [0; 128];
-        let config = self.generate_config_descriptor(&mut config_descriptor_buffer)?;
+    ) -> Result<()> {
+        let config = self
+            .generate_config_descriptor(instance_hash)
+            .map_err(|_| diced_open_dice::DiceError::InvalidInput)?;
 
         let dice_inputs = InputValues::new(
             self.code_hash,
-            Config::Descriptor(config),
+            Config::Descriptor(&config),
             self.auth_hash,
             self.mode,
-            self.make_hidden(salt)?,
+            self.make_hidden(salt, deferred_rollback_protection)?,
         );
         let _ = bcc_handover_main_flow(current_bcc_handover, &dice_inputs, next_bcc)?;
         Ok(())
     }
 
-    fn make_hidden(&self, salt: &[u8; HIDDEN_SIZE]) -> diced_open_dice::Result<[u8; HIDDEN_SIZE]> {
+    fn make_hidden(
+        &self,
+        salt: &[u8; HIDDEN_SIZE],
+        deferred_rollback_protection: bool,
+    ) -> diced_open_dice::Result<[u8; HIDDEN_SIZE]> {
         // We want to make sure we get a different sealing CDI for:
         // - VMs with different salt values
         // - An RKP VM and any other VM (regardless of salt)
+        // - depending on whether rollback protection has been deferred to payload. This ensures the
+        //   adversary cannot leak the secrets by using old images & setting
+        //   `deferred_rollback_protection` to true.
         // The hidden input for DICE affects the sealing CDI (but the values in the config
         // descriptor do not).
         // Since the hidden input has to be a fixed size, create it as a hash of the values we
@@ -92,26 +135,34 @@
         struct HiddenInput {
             rkp_vm_marker: bool,
             salt: [u8; HIDDEN_SIZE],
+            deferred_rollback_protection: bool,
         }
-        // TODO(b/291213394): Include `defer_rollback_protection` flag in the Hidden Input to
-        // differentiate the secrets in both cases.
-        hash(HiddenInput { rkp_vm_marker: self.rkp_vm_marker, salt: *salt }.as_bytes())
+        hash(
+            HiddenInput {
+                rkp_vm_marker: self.rkp_vm_marker,
+                salt: *salt,
+                deferred_rollback_protection,
+            }
+            .as_bytes(),
+        )
     }
 
-    fn generate_config_descriptor<'a>(
-        &self,
-        config_descriptor_buffer: &'a mut [u8],
-    ) -> diced_open_dice::Result<&'a [u8]> {
-        let config_values = DiceConfigValues {
-            component_name: Some(cstr!("vm_entry")),
-            security_version: if cfg!(dice_changes) { Some(self.security_version) } else { None },
-            rkp_vm_marker: self.rkp_vm_marker,
-            ..Default::default()
-        };
-        let config_descriptor_size =
-            bcc_format_config_descriptor(&config_values, config_descriptor_buffer)?;
-        let config = &config_descriptor_buffer[..config_descriptor_size];
-        Ok(config)
+    fn generate_config_descriptor(&self, instance_hash: Option<Hash>) -> Result<Vec<u8>> {
+        let mut config = Vec::with_capacity(4);
+        config.push((cbor!(COMPONENT_NAME_KEY)?, cbor!("vm_entry")?));
+        if cfg!(dice_changes) {
+            config.push((cbor!(SECURITY_VERSION_KEY)?, cbor!(self.security_version)?));
+        }
+        if self.rkp_vm_marker {
+            config.push((cbor!(RKP_VM_MARKER_KEY)?, Value::Null))
+        }
+        if let Some(instance_hash) = instance_hash {
+            config.push((cbor!(INSTANCE_HASH_KEY)?, Value::from(instance_hash.as_slice())));
+        }
+        let config = Value::Map(config);
+        Ok(cbor_util::serialize(&config).map_err(|e| {
+            ciborium::value::Error::Custom(format!("Error in serialization: {e:?}"))
+        })?)
     }
 }
 
@@ -140,17 +191,24 @@
 
 #[cfg(test)]
 mod tests {
-    use super::*;
+    use crate::{
+        Hash, PartialInputs, COMPONENT_NAME_KEY, INSTANCE_HASH_KEY, RKP_VM_MARKER_KEY,
+        SECURITY_VERSION_KEY,
+    };
     use ciborium::Value;
+    use diced_open_dice::DiceArtifacts;
+    use diced_open_dice::DiceMode;
+    use diced_open_dice::HIDDEN_SIZE;
+    use pvmfw_avb::Capability;
+    use pvmfw_avb::DebugLevel;
+    use pvmfw_avb::Digest;
+    use pvmfw_avb::VerifiedBootData;
     use std::collections::HashMap;
+    use std::mem::size_of;
     use std::vec;
 
-    const COMPONENT_NAME_KEY: i64 = -70002;
     const COMPONENT_VERSION_KEY: i64 = -70003;
     const RESETTABLE_KEY: i64 = -70004;
-    const SECURITY_VERSION_KEY: i64 = -70005;
-    const RKP_VM_MARKER_KEY: i64 = -70006;
-
     const BASE_VB_DATA: VerifiedBootData = VerifiedBootData {
         debug_level: DebugLevel::None,
         kernel_digest: [1u8; size_of::<Digest>()],
@@ -159,6 +217,7 @@
         capabilities: vec![],
         rollback_index: 42,
     };
+    const HASH: Hash = *b"sixtyfourbyteslongsentencearerarebutletsgiveitatrycantbethathard";
 
     #[test]
     fn base_data_conversion() {
@@ -193,7 +252,7 @@
     fn base_config_descriptor() {
         let vb_data = BASE_VB_DATA;
         let inputs = PartialInputs::new(&vb_data).unwrap();
-        let config_map = decode_config_descriptor(&inputs);
+        let config_map = decode_config_descriptor(&inputs, None);
 
         assert_eq!(config_map.get(&COMPONENT_NAME_KEY).unwrap().as_text().unwrap(), "vm_entry");
         assert_eq!(config_map.get(&COMPONENT_VERSION_KEY), None);
@@ -214,21 +273,104 @@
         let vb_data =
             VerifiedBootData { capabilities: vec![Capability::RemoteAttest], ..BASE_VB_DATA };
         let inputs = PartialInputs::new(&vb_data).unwrap();
-        let config_map = decode_config_descriptor(&inputs);
+        let config_map = decode_config_descriptor(&inputs, Some(HASH));
 
         assert!(config_map.get(&RKP_VM_MARKER_KEY).unwrap().is_null());
     }
 
-    fn decode_config_descriptor(inputs: &PartialInputs) -> HashMap<i64, Value> {
-        let mut buffer = [0; 128];
-        let config_descriptor = inputs.generate_config_descriptor(&mut buffer).unwrap();
+    #[test]
+    fn config_descriptor_with_instance_hash() {
+        let vb_data =
+            VerifiedBootData { capabilities: vec![Capability::RemoteAttest], ..BASE_VB_DATA };
+        let inputs = PartialInputs::new(&vb_data).unwrap();
+        let config_map = decode_config_descriptor(&inputs, Some(HASH));
+        assert_eq!(*config_map.get(&INSTANCE_HASH_KEY).unwrap(), Value::from(HASH.as_slice()));
+    }
+
+    #[test]
+    fn config_descriptor_without_instance_hash() {
+        let vb_data =
+            VerifiedBootData { capabilities: vec![Capability::RemoteAttest], ..BASE_VB_DATA };
+        let inputs = PartialInputs::new(&vb_data).unwrap();
+        let config_map = decode_config_descriptor(&inputs, None);
+        assert!(!config_map.contains_key(&INSTANCE_HASH_KEY));
+    }
+
+    fn decode_config_descriptor(
+        inputs: &PartialInputs,
+        instance_hash: Option<Hash>,
+    ) -> HashMap<i64, Value> {
+        let config_descriptor = inputs.generate_config_descriptor(instance_hash).unwrap();
 
         let cbor_map =
-            cbor_util::deserialize::<Value>(config_descriptor).unwrap().into_map().unwrap();
+            cbor_util::deserialize::<Value>(&config_descriptor).unwrap().into_map().unwrap();
 
         cbor_map
             .into_iter()
             .map(|(k, v)| ((k.into_integer().unwrap().try_into().unwrap()), v))
             .collect()
     }
+
+    #[test]
+    fn changing_deferred_rpb_changes_secrets() {
+        let vb_data = VerifiedBootData { debug_level: DebugLevel::Full, ..BASE_VB_DATA };
+        let inputs = PartialInputs::new(&vb_data).unwrap();
+        let mut buffer_without_defer = [0; 4096];
+        let mut buffer_with_defer = [0; 4096];
+        let mut buffer_without_defer_retry = [0; 4096];
+
+        let sample_dice_input: &[u8] = &[
+            0xa3, // CDI attest
+            0x01, 0x58, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // CDI seal
+            0x02, 0x58, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // DICE chain
+            0x03, 0x82, 0xa6, 0x01, 0x02, 0x03, 0x27, 0x04, 0x02, 0x20, 0x01, 0x21, 0x40, 0x22,
+            0x40, 0x84, 0x40, 0xa0, 0x40, 0x40,
+            // 8-bytes of trailing data that aren't part of the DICE chain.
+            0x84, 0x41, 0x55, 0xa0, 0x42, 0x11, 0x22, 0x40,
+        ];
+
+        inputs
+            .clone()
+            .write_next_bcc(
+                sample_dice_input,
+                &[0u8; HIDDEN_SIZE],
+                Some([0u8; 64]),
+                false,
+                &mut buffer_without_defer,
+            )
+            .unwrap();
+        let bcc_handover1 = diced_open_dice::bcc_handover_parse(&buffer_without_defer).unwrap();
+
+        inputs
+            .clone()
+            .write_next_bcc(
+                sample_dice_input,
+                &[0u8; HIDDEN_SIZE],
+                Some([0u8; 64]),
+                true,
+                &mut buffer_with_defer,
+            )
+            .unwrap();
+        let bcc_handover2 = diced_open_dice::bcc_handover_parse(&buffer_with_defer).unwrap();
+
+        inputs
+            .clone()
+            .write_next_bcc(
+                sample_dice_input,
+                &[0u8; HIDDEN_SIZE],
+                Some([0u8; 64]),
+                false,
+                &mut buffer_without_defer_retry,
+            )
+            .unwrap();
+        let bcc_handover3 =
+            diced_open_dice::bcc_handover_parse(&buffer_without_defer_retry).unwrap();
+
+        assert_ne!(bcc_handover1.cdi_seal(), bcc_handover2.cdi_seal());
+        assert_eq!(bcc_handover1.cdi_seal(), bcc_handover3.cdi_seal());
+    }
 }
diff --git a/pvmfw/src/fdt.rs b/pvmfw/src/fdt.rs
index 9206588..84dc14d 100644
--- a/pvmfw/src/fdt.rs
+++ b/pvmfw/src/fdt.rs
@@ -515,7 +515,7 @@
 impl PciInfo {
     const IRQ_MASK_CELLS: usize = 4;
     const IRQ_MAP_CELLS: usize = 10;
-    const MAX_IRQS: usize = 10;
+    const MAX_IRQS: usize = 16;
 }
 
 type PciAddrRange = AddressRange<(u32, u64), u64, u64>;
diff --git a/pvmfw/src/main.rs b/pvmfw/src/main.rs
index 2af19c4..247aa6a 100644
--- a/pvmfw/src/main.rs
+++ b/pvmfw/src/main.rs
@@ -116,21 +116,6 @@
         info!("Please disregard any previous libavb ERROR about initrd_normal.");
     }
 
-    if verified_boot_data.has_capability(Capability::RemoteAttest) {
-        info!("Service VM capable of remote attestation detected");
-        if service_vm_version::VERSION != verified_boot_data.rollback_index {
-            // For RKP VM, we only boot if the version in the AVB footer of its kernel matches
-            // the one embedded in pvmfw at build time.
-            // This prevents the pvmfw from booting a roll backed RKP VM.
-            error!(
-                "Service VM version mismatch: expected {}, found {}",
-                service_vm_version::VERSION,
-                verified_boot_data.rollback_index
-            );
-            return Err(RebootReason::InvalidPayload);
-        }
-    }
-
     let next_bcc = heap::aligned_boxed_slice(NEXT_BCC_SIZE, GUEST_PAGE_SIZE).ok_or_else(|| {
         error!("Failed to allocate the next-stage BCC");
         RebootReason::InternalError
@@ -143,10 +128,10 @@
         RebootReason::InternalError
     })?;
 
-    let (new_instance, salt) = if cfg!(llpvm_changes)
-        && should_defer_rollback_protection(fdt)?
-        && verified_boot_data.has_capability(Capability::SecretkeeperProtection)
-    {
+    let instance_hash = if cfg!(llpvm_changes) { Some(salt_from_instance_id(fdt)?) } else { None };
+    let defer_rollback_protection = should_defer_rollback_protection(fdt)?
+        && verified_boot_data.has_capability(Capability::SecretkeeperProtection);
+    let (new_instance, salt) = if defer_rollback_protection {
         info!("Guest OS is capable of Secretkeeper protection, deferring rollback protection");
         // rollback_index of the image is used as security_version and is expected to be > 0 to
         // discourage implicit allocation.
@@ -154,28 +139,39 @@
             error!("Expected positive rollback_index, found 0");
             return Err(RebootReason::InvalidPayload);
         };
-        // `new_instance` cannot be known to pvmfw
-        (false, salt_from_instance_id(fdt)?)
+        (false, instance_hash.unwrap())
+    } else if verified_boot_data.has_capability(Capability::RemoteAttest) {
+        info!("Service VM capable of remote attestation detected, performing version checks");
+        if service_vm_version::VERSION != verified_boot_data.rollback_index {
+            // For RKP VM, we only boot if the version in the AVB footer of its kernel matches
+            // the one embedded in pvmfw at build time.
+            // This prevents the pvmfw from booting a roll backed RKP VM.
+            error!(
+                "Service VM version mismatch: expected {}, found {}",
+                service_vm_version::VERSION,
+                verified_boot_data.rollback_index
+            );
+            return Err(RebootReason::InvalidPayload);
+        }
+        (false, instance_hash.unwrap())
     } else {
+        info!("Fallback to instance.img based rollback checks");
         let (recorded_entry, mut instance_img, header_index) =
             get_recorded_entry(&mut pci_root, cdi_seal).map_err(|e| {
                 error!("Failed to get entry from instance.img: {e}");
                 RebootReason::InternalError
             })?;
         let (new_instance, salt) = if let Some(entry) = recorded_entry {
-            maybe_check_dice_measurements_match_entry(&dice_inputs, &entry)?;
-            let salt = if cfg!(llpvm_changes) { salt_from_instance_id(fdt)? } else { entry.salt };
+            check_dice_measurements_match_entry(&dice_inputs, &entry)?;
+            let salt = instance_hash.unwrap_or(entry.salt);
             (false, salt)
         } else {
             // New instance!
-            let salt = if cfg!(llpvm_changes) {
-                salt_from_instance_id(fdt)?
-            } else {
-                rand::random_array().map_err(|e| {
-                    error!("Failed to generated instance.img salt: {e}");
-                    RebootReason::InternalError
-                })?
-            };
+            let salt = instance_hash.map_or_else(rand::random_array, Ok).map_err(|e| {
+                error!("Failed to generated instance.img salt: {e}");
+                RebootReason::InternalError
+            })?;
+
             let entry = EntryBody::new(&dice_inputs, &salt);
             record_instance_entry(&entry, cdi_seal, &mut instance_img, header_index).map_err(
                 |e| {
@@ -204,10 +200,18 @@
         Cow::Owned(truncated_bcc_handover)
     };
 
-    dice_inputs.write_next_bcc(new_bcc_handover.as_ref(), &salt, next_bcc).map_err(|e| {
-        error!("Failed to derive next-stage DICE secrets: {e:?}");
-        RebootReason::SecretDerivationError
-    })?;
+    dice_inputs
+        .write_next_bcc(
+            new_bcc_handover.as_ref(),
+            &salt,
+            instance_hash,
+            defer_rollback_protection,
+            next_bcc,
+        )
+        .map_err(|e| {
+            error!("Failed to derive next-stage DICE secrets: {e:?}");
+            RebootReason::SecretDerivationError
+        })?;
     flush(next_bcc);
 
     let kaslr_seed = u64::from_ne_bytes(rand::random_array().map_err(|e| {
@@ -239,21 +243,10 @@
     Ok(bcc_range)
 }
 
-fn maybe_check_dice_measurements_match_entry(
+fn check_dice_measurements_match_entry(
     dice_inputs: &PartialInputs,
     entry: &EntryBody,
 ) -> Result<(), RebootReason> {
-    // The RKP VM is allowed to run if it has passed the verified boot check and
-    // contains the expected version in its AVB footer.
-    // The comparison below with the previous boot information is skipped to enable the
-    // simultaneous update of the pvmfw and RKP VM.
-    // For instance, when both the pvmfw and RKP VM are updated, the code hash of the
-    // RKP VM will differ from the one stored in the instance image. In this case, the
-    // RKP VM is still allowed to run.
-    // This ensures that the updated RKP VM will retain the same CDIs in the next stage.
-    if dice_inputs.rkp_vm_marker {
-        return Ok(());
-    }
     ensure_dice_measurements_match_entry(dice_inputs, entry).map_err(|e| {
         error!(
             "Dice measurements do not match recorded entry. \
diff --git a/service_vm/client_vm_csr/Android.bp b/service_vm/client_vm_csr/Android.bp
index 8d738d8..097779f 100644
--- a/service_vm/client_vm_csr/Android.bp
+++ b/service_vm/client_vm_csr/Android.bp
@@ -8,6 +8,7 @@
     srcs: ["src/lib.rs"],
     rustlibs: [
         "libanyhow",
+        "libcbor_util",
         "libcoset",
         "libdiced_open_dice",
         "libopenssl",
diff --git a/service_vm/client_vm_csr/src/lib.rs b/service_vm/client_vm_csr/src/lib.rs
index 0babfff..70152cb 100644
--- a/service_vm/client_vm_csr/src/lib.rs
+++ b/service_vm/client_vm_csr/src/lib.rs
@@ -20,7 +20,9 @@
     iana, CborSerializable, CoseKey, CoseKeyBuilder, CoseSign, CoseSignBuilder, CoseSignature,
     CoseSignatureBuilder, HeaderBuilder,
 };
-use diced_open_dice::{derive_cdi_leaf_priv, sign, DiceArtifacts, PrivateKey};
+use diced_open_dice::{
+    derive_cdi_leaf_priv, sign, DiceArtifacts, PrivateKey, DICE_COSE_KEY_ALG_VALUE,
+};
 use openssl::{
     bn::{BigNum, BigNumContext},
     ec::{EcGroup, EcKey, EcKeyRef},
@@ -91,7 +93,8 @@
     cdi_leaf_priv: &PrivateKey,
     attestation_key: &EcKeyRef<Private>,
 ) -> Result<CoseSign> {
-    let cdi_leaf_sig_headers = build_signature_headers(iana::Algorithm::EdDSA);
+    let dice_key_alg = cbor_util::dice_cose_key_alg(DICE_COSE_KEY_ALG_VALUE)?;
+    let cdi_leaf_sig_headers = build_signature_headers(dice_key_alg);
     let attestation_key_sig_headers = build_signature_headers(ATTESTATION_KEY_ALGO);
     let aad = &[];
     let signed_data = CoseSignBuilder::new()
diff --git a/service_vm/comm/src/client_vm_csr.cddl b/service_vm/comm/src/client_vm_csr.cddl
index bbc709a..7ddbfa3 100644
--- a/service_vm/comm/src/client_vm_csr.cddl
+++ b/service_vm/comm/src/client_vm_csr.cddl
@@ -33,9 +33,10 @@
 
 ; COSE_Signature [RFC9052 s4.1]
 COSE_Signature_Dice_Cdi_Leaf = [
-    protected: bstr .cbor { 1: AlgorithmEdDSA },
+    protected: bstr .cbor { 1: AlgorithmEdDSA / AlgorithmES256 / AlgorithmES384 },
     unprotected: {},
-    signature: bstr,                         ; Ed25519(CDI_Leaf_Priv, SigStruct)
+    signature: bstr,                         ; PureEd25519(CDI_Leaf_Priv, SigStruct)
+                                             ; ECDSA(CDI_Leaf_Priv, SigStruct)
 ]
 
 ; COSE_Signature [RFC9052 s4.1]
diff --git a/service_vm/demo_apk/Android.bp b/service_vm/demo_apk/Android.bp
index 3750fe6..c64b70a 100644
--- a/service_vm/demo_apk/Android.bp
+++ b/service_vm/demo_apk/Android.bp
@@ -23,7 +23,7 @@
         "libandroid_logger",
         "libanyhow",
         "liblog_rust",
-        "libvm_payload_bindgen",
+        "libvm_payload_rs",
     ],
 }
 
diff --git a/service_vm/demo_apk/src/main.rs b/service_vm/demo_apk/src/main.rs
index 8ea4e65..26df52c 100644
--- a/service_vm/demo_apk/src/main.rs
+++ b/service_vm/demo_apk/src/main.rs
@@ -14,25 +14,15 @@
 
 //! Main executable of Service VM client for manual testing.
 
-use anyhow::{anyhow, ensure, Result};
+use anyhow::{ensure, Context, Result};
 use log::{error, info};
-use std::{
-    ffi::{c_void, CStr},
-    panic,
-    ptr::{self, NonNull},
-    result,
-};
-use vm_payload_bindgen::{
-    AVmAttestationResult, AVmAttestationResult_free, AVmAttestationResult_getCertificateAt,
-    AVmAttestationResult_getCertificateCount, AVmAttestationResult_getPrivateKey,
-    AVmAttestationResult_sign, AVmAttestationStatus, AVmAttestationStatus_toString,
-    AVmPayload_requestAttestation,
-};
+use std::panic;
+use vm_payload::AttestationError;
+
+vm_payload::main!(main);
 
 /// Entry point of the Service VM client.
-#[allow(non_snake_case)]
-#[no_mangle]
-pub extern "C" fn AVmPayload_main() {
+fn main() {
     android_logger::init_once(
         android_logger::Config::default()
             .with_tag("service_vm_client")
@@ -52,15 +42,11 @@
     info!("Welcome to Service VM Client!");
 
     let too_big_challenge = &[0u8; 66];
-    let res = AttestationResult::request_attestation(too_big_challenge);
+    let res = vm_payload::request_attestation(too_big_challenge);
     ensure!(res.is_err());
-    let status = res.unwrap_err();
-    ensure!(
-        status == AVmAttestationStatus::ATTESTATION_ERROR_INVALID_CHALLENGE,
-        "Unexpected status: {:?}",
-        status
-    );
-    info!("Status: {:?}", status_to_cstr(status));
+    let error = res.unwrap_err();
+    ensure!(error == AttestationError::InvalidChallenge, "Unexpected error: {error:?}");
+    info!("Error: {error}");
 
     // The data below is only a placeholder generated randomly with urandom
     let challenge = &[
@@ -68,162 +54,18 @@
         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)))?;
+    let res = vm_payload::request_attestation(challenge).context("Unexpected attestation error")?;
 
-    let cert_chain = res.certificate_chain()?;
+    let cert_chain: Vec<_> = res.certificate_chain().collect();
     info!("Attestation result certificateChain = {:?}", cert_chain);
 
-    let private_key = res.private_key()?;
+    let private_key = res.private_key();
     info!("Attestation result privateKey = {:?}", private_key);
 
     let message = b"Hello from Service VM client";
     info!("Signing message: {:?}", message);
-    let signature = res.sign(message)?;
+    let signature = res.sign_message(message);
     info!("Signature: {:?}", signature);
 
     Ok(())
 }
-
-#[derive(Debug)]
-struct AttestationResult(NonNull<AVmAttestationResult>);
-
-impl AttestationResult {
-    fn request_attestation(challenge: &[u8]) -> result::Result<Self, AVmAttestationStatus> {
-        let mut res: *mut AVmAttestationResult = ptr::null_mut();
-        // SAFETY: It is safe as we only read the challenge within its bounds and the
-        // function does not retain any reference to it.
-        let status = unsafe {
-            AVmPayload_requestAttestation(
-                challenge.as_ptr() as *const c_void,
-                challenge.len(),
-                &mut res,
-            )
-        };
-        if status == AVmAttestationStatus::ATTESTATION_OK {
-            info!("Attestation succeeds. Status: {:?}", status_to_cstr(status));
-            let res = NonNull::new(res).expect("The attestation result is null");
-            Ok(Self(res))
-        } else {
-            Err(status)
-        }
-    }
-
-    fn certificate_chain(&self) -> Result<Vec<Box<[u8]>>> {
-        let num_certs = get_certificate_count(self.as_ref());
-        let mut certs = Vec::with_capacity(num_certs);
-        for i in 0..num_certs {
-            certs.push(get_certificate_at(self.as_ref(), i)?);
-        }
-        Ok(certs)
-    }
-
-    fn private_key(&self) -> Result<Box<[u8]>> {
-        get_private_key(self.as_ref())
-    }
-
-    fn sign(&self, message: &[u8]) -> Result<Box<[u8]>> {
-        sign_with_attested_key(self.as_ref(), message)
-    }
-}
-
-impl AsRef<AVmAttestationResult> for AttestationResult {
-    fn as_ref(&self) -> &AVmAttestationResult {
-        // SAFETY: This field is private, and only populated with a successful call to
-        // `AVmPayload_requestAttestation`.
-        unsafe { self.0.as_ref() }
-    }
-}
-
-impl Drop for AttestationResult {
-    fn drop(&mut self) {
-        // SAFETY: This field is private, and only populated with a successful call to
-        // `AVmPayload_requestAttestation`, and not freed elsewhere.
-        unsafe { AVmAttestationResult_free(self.0.as_ptr()) };
-    }
-}
-
-fn get_certificate_count(res: &AVmAttestationResult) -> usize {
-    // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
-    // before getting freed.
-    unsafe { AVmAttestationResult_getCertificateCount(res) }
-}
-
-fn get_certificate_at(res: &AVmAttestationResult, index: usize) -> Result<Box<[u8]>> {
-    let size =
-        // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
-        // before getting freed.
-        unsafe { AVmAttestationResult_getCertificateAt(res, index, ptr::null_mut(), 0) };
-    let mut cert = vec![0u8; size];
-    // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
-    // before getting freed. This function only writes within the bounds of `cert`.
-    // And `cert` cannot overlap `res` because we just allocated it.
-    let size = unsafe {
-        AVmAttestationResult_getCertificateAt(
-            res,
-            index,
-            cert.as_mut_ptr() as *mut c_void,
-            cert.len(),
-        )
-    };
-    ensure!(size == cert.len());
-    Ok(cert.into_boxed_slice())
-}
-
-fn get_private_key(res: &AVmAttestationResult) -> Result<Box<[u8]>> {
-    let size =
-        // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
-        // before getting freed.
-        unsafe { AVmAttestationResult_getPrivateKey(res, ptr::null_mut(), 0) };
-    let mut private_key = vec![0u8; size];
-    // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
-    // before getting freed. This function only writes within the bounds of `private_key`.
-    // And `private_key` cannot overlap `res` because we just allocated it.
-    let size = unsafe {
-        AVmAttestationResult_getPrivateKey(
-            res,
-            private_key.as_mut_ptr() as *mut c_void,
-            private_key.len(),
-        )
-    };
-    ensure!(size == private_key.len());
-    Ok(private_key.into_boxed_slice())
-}
-
-fn sign_with_attested_key(res: &AVmAttestationResult, message: &[u8]) -> Result<Box<[u8]>> {
-    // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
-    // before getting freed.
-    let size = unsafe {
-        AVmAttestationResult_sign(
-            res,
-            message.as_ptr() as *const c_void,
-            message.len(),
-            ptr::null_mut(),
-            0,
-        )
-    };
-    let mut 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());
-    Ok(signature.into_boxed_slice())
-}
-
-fn status_to_cstr(status: AVmAttestationStatus) -> &'static CStr {
-    // SAFETY: The function only reads the given enum status and returns a pointer to a
-    // static string.
-    let message = unsafe { AVmAttestationStatus_toString(status) };
-    // SAFETY: The pointer returned by `AVmAttestationStatus_toString` is guaranteed to
-    // point to a valid C String that lives forever.
-    unsafe { CStr::from_ptr(message) }
-}
diff --git a/service_vm/requests/src/rkp.rs b/service_vm/requests/src/rkp.rs
index 4f2262f..c62a36b 100644
--- a/service_vm/requests/src/rkp.rs
+++ b/service_vm/requests/src/rkp.rs
@@ -26,8 +26,10 @@
     value::{CanonicalValue, Value},
 };
 use core::result;
-use coset::{iana, AsCborValue, CoseSign1, CoseSign1Builder, HeaderBuilder};
-use diced_open_dice::{derive_cdi_leaf_priv, kdf, sign, DiceArtifacts, PrivateKey};
+use coset::{AsCborValue, CoseSign1, CoseSign1Builder, HeaderBuilder};
+use diced_open_dice::{
+    derive_cdi_leaf_priv, kdf, sign, DiceArtifacts, PrivateKey, DICE_COSE_KEY_ALG_VALUE,
+};
 use log::{debug, error};
 use service_vm_comm::{EcdsaP256KeyPair, GenerateCertificateRequestParams, RequestProcessingError};
 use zeroize::Zeroizing;
@@ -125,7 +127,7 @@
         "product" => "avf",
         "vb_state" => "avf",
         "manufacturer" => "aosp-avf",
-        "vbmeta_digest" => Value::Bytes(vec![1u8; 0]),
+        "vbmeta_digest" => Value::Bytes(vec![1u8; 1]),
         "security_level" => "avf",
         "boot_patch_level" => 20240202,
         "bootloader_state" => "avf",
@@ -151,8 +153,8 @@
         error!("Failed to derive the CDI_Leaf_Priv: {e}");
         RequestProcessingError::InternalError
     })?;
-    let signing_algorithm = iana::Algorithm::EdDSA;
-    let protected = HeaderBuilder::new().algorithm(signing_algorithm).build();
+    let dice_key_alg = cbor_util::dice_cose_key_alg(DICE_COSE_KEY_ALG_VALUE)?;
+    let protected = HeaderBuilder::new().algorithm(dice_key_alg).build();
     let signed_data = CoseSign1Builder::new()
         .protected(protected)
         .payload(cbor_util::serialize(payload)?)
diff --git a/service_vm/test_apk/Android.bp b/service_vm/test_apk/Android.bp
index 1ba156f..58b394a 100644
--- a/service_vm/test_apk/Android.bp
+++ b/service_vm/test_apk/Android.bp
@@ -39,7 +39,7 @@
         "libanyhow",
         "libavflog",
         "liblog_rust",
-        "libvm_payload_bindgen",
+        "libvm_payload_rs",
     ],
 }
 
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
index af99711..ff760b4 100644
--- 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
@@ -69,6 +69,9 @@
                 .that(isCuttlefish())
                 .isFalse();
         assumeFeatureEnabled(VirtualMachineManager.FEATURE_REMOTE_ATTESTATION);
+        assume().withMessage("Test needs Remote Attestation support")
+                .that(getVirtualMachineManager().isRemoteAttestationSupported())
+                .isTrue();
 
         VirtualMachineConfig.Builder builder =
                 newVmConfigBuilderWithPayloadBinary(VM_PAYLOAD_PATH)
diff --git a/service_vm/test_apk/src/native/main.rs b/service_vm/test_apk/src/native/main.rs
index 00ddff8..52635ad 100644
--- a/service_vm/test_apk/src/native/main.rs
+++ b/service_vm/test_apk/src/native/main.rs
@@ -14,35 +14,26 @@
 
 //! Main executable of VM attestation for end-to-end testing.
 
-use anyhow::{anyhow, ensure, Result};
+use anyhow::Result;
 use avflog::LogResult;
 use com_android_virt_vm_attestation_testservice::{
     aidl::com::android::virt::vm_attestation::testservice::IAttestationService::{
         AttestationStatus::AttestationStatus, BnAttestationService, IAttestationService,
         SigningResult::SigningResult, PORT,
     },
-    binder::{self, unstable_api::AsNative, BinderFeatures, Interface, IntoBinderResult, Strong},
+    binder::{self, 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::{
-    AIBinder, AVmAttestationResult, AVmAttestationResult_free,
-    AVmAttestationResult_getCertificateAt, AVmAttestationResult_getCertificateCount,
-    AVmAttestationResult_getPrivateKey, AVmAttestationResult_sign, AVmAttestationStatus,
-    AVmAttestationStatus_toString, AVmPayload_notifyPayloadReady, AVmPayload_requestAttestation,
-    AVmPayload_requestAttestationForTesting, AVmPayload_runVsockRpcServer,
-};
+use vm_payload::{AttestationError, AttestationResult};
 
-/// Entry point of the Service VM client.
-#[allow(non_snake_case)]
-#[no_mangle]
-pub extern "C" fn AVmPayload_main() {
+vm_payload::main!(main);
+
+// Entry point of the Service VM client.
+fn main() {
     android_logger::init_once(
         android_logger::Config::default()
             .with_tag("service_vm_client")
@@ -61,18 +52,7 @@
 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() };
+    vm_payload::run_single_vsock_service(AttestationService::new_binder(), PORT.try_into()?)
 }
 
 struct AttestationService {
@@ -88,11 +68,11 @@
     }
 }
 
+#[allow(non_snake_case)]
 impl IAttestationService for AttestationService {
     fn requestAttestationForTesting(&self) -> binder::Result<()> {
         const CHALLENGE: &[u8] = &[0xaa; 32];
-        let res = AttestationResult::request_attestation_for_testing(CHALLENGE)
-            .map_err(|e| anyhow!("Unexpected status: {:?}", status_to_cstr(e)))
+        let res = vm_payload::restricted::request_attestation_for_testing(CHALLENGE)
             .with_log()
             .or_service_specific_exception(-1)?;
         *self.res.lock().unwrap() = Some(res);
@@ -104,218 +84,46 @@
         challenge: &[u8],
         message: &[u8],
     ) -> binder::Result<SigningResult> {
-        let res = match AttestationResult::request_attestation(challenge) {
+        let res: AttestationResult = match vm_payload::request_attestation(challenge) {
             Ok(res) => res,
-            Err(status) => {
-                let status = to_attestation_status(status);
+            Err(e) => {
+                let status = to_attestation_status(e);
                 return Ok(SigningResult { certificateChain: vec![], signature: vec![], status });
             }
         };
-        let certificate_chain =
-            res.certificate_chain().with_log().or_service_specific_exception(-1)?;
+
+        let certificate_chain: Vec<u8> = res.certificate_chain().flatten().collect();
         let status = AttestationStatus::OK;
-        let signature = res.sign(message).with_log().or_service_specific_exception(-1)?;
+        let signature = res.sign_message(message);
+
         Ok(SigningResult { certificateChain: certificate_chain, signature, status })
     }
 
     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)
-    }
-}
-
-fn to_attestation_status(status: AVmAttestationStatus) -> AttestationStatus {
-    match status {
-        AVmAttestationStatus::ATTESTATION_OK => AttestationStatus::OK,
-        AVmAttestationStatus::ATTESTATION_ERROR_INVALID_CHALLENGE => {
-            AttestationStatus::ERROR_INVALID_CHALLENGE
-        }
-        AVmAttestationStatus::ATTESTATION_ERROR_ATTESTATION_FAILED => {
-            AttestationStatus::ERROR_ATTESTATION_FAILED
-        }
-        AVmAttestationStatus::ATTESTATION_ERROR_UNSUPPORTED => AttestationStatus::ERROR_UNSUPPORTED,
-    }
-}
-
-#[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_for_testing(
-        challenge: &[u8],
-    ) -> result::Result<Self, AVmAttestationStatus> {
-        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 == AVmAttestationStatus::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 request_attestation(challenge: &[u8]) -> result::Result<Self, AVmAttestationStatus> {
-        let mut res: *mut AVmAttestationResult = ptr::null_mut();
-        // SAFETY: It is safe as we only read the challenge within its bounds and the
-        // function does not retain any reference to it.
-        let status = unsafe {
-            AVmPayload_requestAttestation(
-                challenge.as_ptr() as *const c_void,
-                challenge.len(),
-                &mut res,
-            )
-        };
-        if status == AVmAttestationStatus::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<u8>> {
-        let num_certs = get_certificate_count(self.as_ref());
-        let mut certs = Vec::new();
-        for i in 0..num_certs {
-            certs.extend(get_certificate_at(self.as_ref(), i)?.iter());
-        }
-        Ok(certs)
-    }
-
-    fn private_key(&self) -> Result<Box<[u8]>> {
-        get_private_key(self.as_ref())
-    }
-
-    fn sign(&self, message: &[u8]) -> Result<Vec<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);
+        log(self.res.lock().unwrap().as_ref().unwrap());
         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() }
+fn log(res: &AttestationResult) {
+    for (i, cert) in res.certificate_chain().enumerate() {
+        info!("Attestation result certificate {i} = {cert:?}");
     }
+
+    let private_key = res.private_key();
+    info!("Attestation result privateKey = {private_key:?}");
+
+    let message = b"Hello from Service VM client";
+    info!("Signing message: {message:?}");
+    let signature = res.sign_message(message);
+    info!("Signature: {signature:?}");
 }
 
-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 to_attestation_status(e: AttestationError) -> AttestationStatus {
+    match e {
+        AttestationError::InvalidChallenge => AttestationStatus::ERROR_INVALID_CHALLENGE,
+        AttestationError::AttestationFailed => AttestationStatus::ERROR_ATTESTATION_FAILED,
+        AttestationError::AttestationUnsupported => AttestationStatus::ERROR_UNSUPPORTED,
     }
 }
-
-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<Vec<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)
-}
-
-fn status_to_cstr(status: AVmAttestationStatus) -> &'static CStr {
-    // SAFETY: The function only reads the given enum status and returns a pointer to a
-    // static string.
-    let message = unsafe { AVmAttestationStatus_toString(status) };
-    // SAFETY: The pointer returned by `AVmAttestationStatus_toString` is guaranteed to
-    // point to a valid C String that lives forever.
-    unsafe { CStr::from_ptr(message) }
-}
diff --git a/tests/benchmark/Android.bp b/tests/benchmark/Android.bp
index 413ffe4..5ede699 100644
--- a/tests/benchmark/Android.bp
+++ b/tests/benchmark/Android.bp
@@ -22,8 +22,10 @@
         "MicrodroidTestNativeLib",
         "libiovsock_host_jni",
     ],
-    jni_uses_platform_apis: true,
-    sdk_version: "test_current",
+    libs: [
+        "framework-virtualization.impl",
+    ],
+    platform_apis: true,
     use_embedded_native_libs: true,
     compile_multilib: "64",
     required: ["perf-setup"],
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 9cc1b7b..c33d3f5 100644
--- a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
+++ b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
@@ -16,8 +16,8 @@
 
 package com.android.microdroid.benchmark;
 
-import static android.system.virtualmachine.VirtualMachineConfig.CPU_TOPOLOGY_ONE_CPU;
 import static android.system.virtualmachine.VirtualMachineConfig.CPU_TOPOLOGY_MATCH_HOST;
+import static android.system.virtualmachine.VirtualMachineConfig.CPU_TOPOLOGY_ONE_CPU;
 import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_FULL;
 import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_NONE;
 
@@ -34,10 +34,10 @@
 import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
 import android.os.Process;
 import android.os.RemoteException;
+import android.system.Os;
 import android.system.virtualmachine.VirtualMachine;
 import android.system.virtualmachine.VirtualMachineConfig;
 import android.system.virtualmachine.VirtualMachineException;
-import android.system.Os;
 import android.system.virtualmachine.VirtualMachineManager;
 import android.util.Log;
 
@@ -158,6 +158,7 @@
                 newVmConfigBuilderWithPayloadBinary("MicrodroidIdleNativeLib.so")
                         .setDebugLevel(DEBUG_LEVEL_NONE)
                         .setMemoryBytes(mem * ONE_MEBI)
+                        .setShouldUseHugepages(true)
                         .build();
 
         // returns true if succeeded at least once.
@@ -233,6 +234,8 @@
         for (int i = 0; i < trialCount; i++) {
             VirtualMachineConfig.Builder builder =
                     newVmConfigBuilderWithPayloadBinary("MicrodroidIdleNativeLib.so")
+                            .setShouldBoostUclamp(true)
+                            .setShouldUseHugepages(true)
                             .setMemoryBytes(256 * ONE_MEBI)
                             .setDebugLevel(DEBUG_LEVEL_NONE);
             if (fullDebug) {
@@ -345,6 +348,8 @@
         VirtualMachineConfig config =
                 newVmConfigBuilderWithPayloadConfig("assets/vm_config_io.json")
                         .setDebugLevel(DEBUG_LEVEL_NONE)
+                        .setShouldBoostUclamp(true)
+                        .setShouldUseHugepages(true)
                         .build();
         List<Double> transferRates = new ArrayList<>(IO_TEST_TRIAL_COUNT);
 
@@ -371,6 +376,7 @@
         VirtualMachineConfig config =
                 newVmConfigBuilderWithPayloadConfig("assets/vm_config_io.json")
                         .setDebugLevel(DEBUG_LEVEL_NONE)
+                        .setShouldUseHugepages(true)
                         .build();
         List<Double> readRates = new ArrayList<>(IO_TEST_TRIAL_COUNT);
 
@@ -522,6 +528,7 @@
         VirtualMachineConfig config =
                 newVmConfigBuilderWithPayloadConfig("assets/vm_config_io.json")
                         .setDebugLevel(DEBUG_LEVEL_NONE)
+                        .setShouldUseHugepages(true)
                         .setMemoryBytes(256 * ONE_MEBI)
                         .build();
         VirtualMachine vm = forceCreateNewVirtualMachine(vmName, config);
@@ -608,6 +615,7 @@
         VirtualMachineConfig config =
                 newVmConfigBuilderWithPayloadConfig("assets/vm_config_io.json")
                         .setDebugLevel(DEBUG_LEVEL_NONE)
+                        .setShouldUseHugepages(true)
                         .setMemoryBytes(256 * ONE_MEBI)
                         .build();
         VirtualMachine vm = forceCreateNewVirtualMachine(vmName, config);
@@ -727,6 +735,8 @@
         VirtualMachineConfig config =
                 newVmConfigBuilderWithPayloadBinary("MicrodroidTestNativeLib.so")
                         .setDebugLevel(DEBUG_LEVEL_NONE)
+                        .setShouldUseHugepages(true)
+                        .setShouldBoostUclamp(true)
                         .build();
 
         List<Double> requestLatencies = new ArrayList<>(IO_TEST_TRIAL_COUNT * NUM_REQUESTS);
@@ -775,6 +785,7 @@
         VirtualMachineConfig config =
                 newVmConfigBuilderWithPayloadBinary("MicrodroidTestNativeLib.so")
                         .setDebugLevel(DEBUG_LEVEL_NONE)
+                        .setShouldUseHugepages(true)
                         .build();
 
         List<Double> requestLatencies = new ArrayList<>(IO_TEST_TRIAL_COUNT * NUM_REQUESTS);
@@ -833,6 +844,7 @@
         VirtualMachineConfig config =
                 newVmConfigBuilderWithPayloadConfig("assets/vm_config_io.json")
                         .setDebugLevel(DEBUG_LEVEL_NONE)
+                        .setShouldUseHugepages(true)
                         .build();
         List<Double> vmKillTime = new ArrayList<>(TEST_TRIAL_COUNT);
 
diff --git a/tests/ferrochrome/Android.bp b/tests/ferrochrome/Android.bp
new file mode 100644
index 0000000..f165b8f
--- /dev/null
+++ b/tests/ferrochrome/Android.bp
@@ -0,0 +1,29 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+sh_test_host {
+    name: "ferrochrome-tests",
+    src: ":ferrochrome-tests.sh",
+    test_suites: ["general-tests"],
+    test_options: {
+        unit_test: false,
+    },
+    per_testcase_directory: true,
+    data: ["assets/vm_config.json"],
+    data_bins: ["ferrochrome-precondition-checker.sh"],
+}
+
+// Workaround for enabling verbose logging only on CI
+genrule {
+    name: "ferrochrome-tests.sh",
+    srcs: ["ferrochrome.sh"],
+    out: ["ferrochrome-tests"],
+    // This breaks shebang, but test will execute the script with bash
+    cmd: "echo \"set -x\" > $(out); cat $(in) >> $(out)",
+}
+
+sh_binary_host {
+    name: "ferrochrome-precondition-checker.sh",
+    src: "ferrochrome-precondition-checker.sh",
+}
diff --git a/tests/ferrochrome/AndroidTest.xml b/tests/ferrochrome/AndroidTest.xml
new file mode 100644
index 0000000..79cbe72
--- /dev/null
+++ b/tests/ferrochrome/AndroidTest.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2024 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Host driven tests for ferrochrome">
+    <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" />
+
+    <!-- 'adb root' to enable vmlauncher -->
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer">
+        <option name="force-root" value="true"/>
+    </target_preparer>
+
+    <!-- Check assumptions here, because we can't skip tests in shell test -->
+    <target_preparer class="com.android.tradefed.targetprep.RunHostScriptTargetPreparer">
+        <option name="script-file" value="ferrochrome-precondition-checker.sh" />
+    </target_preparer>
+
+    <!-- Explicitly clean up ferrochrome image when done.
+         It's too large (6.5G+), so this may break further tests. -->
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        <option name="throw-if-cmd-fail" value="false" />
+        <option name="run-command" value="mkdir /data/local/tmp/ferrochrome" />
+        <option name="teardown-command" value="pkill vmlauncher" />
+        <option name="teardown-command" value="rm -rf /data/local/tmp/ferrochrome" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.binary.ExecutableHostTest" >
+        <option name="binary" value="ferrochrome-tests" />
+        <option name="relative-path-execution" value="true" />
+        <option name="runtime-hint" value="10m" />
+        <option name="per-binary-timeout" value="20m" />
+    </test>
+</configuration>
+
diff --git a/tests/ferrochrome/assets/vm_config.json b/tests/ferrochrome/assets/vm_config.json
new file mode 100644
index 0000000..1d32463
--- /dev/null
+++ b/tests/ferrochrome/assets/vm_config.json
@@ -0,0 +1,17 @@
+{
+    "name": "cros",
+    "disks": [
+        {
+            "image": "/data/local/tmp/ferrochrome/chromiumos_test_image.bin",
+            "partitions": [],
+            "writable": true
+        }
+    ],
+    "params": "root=/dev/vda3 rootwait noinitrd ro enforcing=0 cros_debug cros_secure",
+    "protected": false,
+    "cpu_topology": "match_host",
+    "platform_version": "~1.0",
+    "memory_mib" : 8096,
+    "console_input_device": "ttyS0"
+}
+
diff --git a/tests/ferrochrome/ferrochrome-precondition-checker.sh b/tests/ferrochrome/ferrochrome-precondition-checker.sh
new file mode 100644
index 0000000..d3f7f5a
--- /dev/null
+++ b/tests/ferrochrome/ferrochrome-precondition-checker.sh
@@ -0,0 +1,58 @@
+#!/bin/bash
+
+# Copyright 2024 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+## Precondition checks for running ferrochrome
+## Used by CI for skipping tests.
+
+REQUIRED_DISK_SPACE=7340032    # Requires 7G, while image is 6.5G
+
+# `adb root` always returns exit code 0
+if [[ "$(adb root)" == *"cannot"* ]]; then
+  >&2 echo "Failed to run adb root"
+  exit 1
+fi
+
+# `pm resolve-activity` always returns exit code 0
+resolved_activity=$(adb shell pm resolve-activity -a android.virtualization.VM_LAUNCHER)
+if [[ "${resolved_activity}" == "No activity found" ]]; then
+  >&2 echo "Failed to find vmlauncher"
+  exit 1
+fi
+
+free_space=$(adb shell df /data/local | tail -1 | awk '{print $4}')
+if [[ ${free_space} -lt ${REQUIRED_DISK_SPACE} ]]; then
+  >&2 echo "Insufficient space on DUT. Need ${REQUIRED_DISK_SPACE}, but was ${free_space}"
+  exit 1
+fi
+
+free_space=$(df /tmp | tail -1 | awk '{print $4}')
+if [[ ${free_space} -lt ${REQUIRED_DISK_SPACE} ]]; then
+  >&2 echo "Insufficient space on host. Need ${REQUIRED_DISK_SPACE}, but was ${free_space}"
+  exit 1
+fi
+
+cpu_abi=$(adb shell getprop ro.product.cpu.abi)
+if [[ "${cpu_abi}" != "arm64"* ]]; then
+  >&2 echo "Unsupported architecture. Requires arm64, but was ${cpu_abi}"
+  exit 1
+fi
+
+device=$(adb shell getprop ro.product.vendor.device)
+if [[ "${device}" == "vsock_"* ]]; then
+  >&2 echo "Unsupported device. Cuttlefish isn't supported"
+  exit 1
+fi
diff --git a/tests/ferrochrome/ferrochrome.sh b/tests/ferrochrome/ferrochrome.sh
new file mode 100755
index 0000000..d72e882
--- /dev/null
+++ b/tests/ferrochrome/ferrochrome.sh
@@ -0,0 +1,149 @@
+#!/bin/bash
+
+# Copyright 2024 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+## Booting tests for ferrochrome
+## Keep this file synced with docs/custom_vm.md
+
+set -e
+
+FECR_GS_URL="https://storage.googleapis.com/chromiumos-image-archive/ferrochrome-public"
+FECR_DEFAULT_VERSION="R127-15916.0.0"
+FECR_DEVICE_DIR="/data/local/tmp/ferrochrome"
+FECR_CONFIG_PATH="/data/local/tmp/vm_config.json"  # hardcoded at VmLauncherApp
+FECR_CONSOLE_LOG_PATH="/data/data/\${pkg_name}/files/console.log"
+FECR_BOOT_COMPLETED_LOG="Have fun and send patches!"
+FECR_BOOT_TIMEOUT="300" # 5 minutes (300 seconds)
+ACTION_NAME="android.virtualization.VM_LAUNCHER"
+
+fecr_clean_up() {
+  trap - INT
+
+  if [[ -d ${fecr_dir} && -z ${fecr_keep} ]]; then
+    rm -rf ${fecr_dir}
+  fi
+}
+
+print_usage() {
+  echo "ferochrome: Launches ferrochrome image"
+  echo ""
+  echo "By default, this downloads ferrochrome image with version ${FECR_DEFAULT_VERSION},"
+  echo "launches, and waits for boot completed."
+  echo "When done, removes downloaded image on host while keeping pushed image on device."
+  echo ""
+  echo "Usage: ferrochrome [options]"
+  echo ""
+  echo "Options"
+  echo "  --help or -h: This message"
+  echo "  --dir DIR: Use ferrochrome images at the dir instead of downloading"
+  echo "  --verbose: Verbose log message (set -x)"
+  echo "  --skip: Skipping downloading and/or pushing images"
+  echo "  --version \${version}: ferrochrome version to be downloaded"
+  echo "  --keep: Keep downloaded ferrochrome image"
+}
+
+fecr_version=""
+fecr_dir=""
+fecr_keep=""
+fecr_skip=""
+fecr_script_path=$(dirname ${0})
+fecr_verbose=""
+
+# Parse parameters
+while (( "${#}" )); do
+  case "${1}" in
+    --verbose)
+      fecr_verbose="true"
+      ;;
+    --version)
+      shift
+      fecr_version="${1}"
+      ;;
+    --dir)
+      shift
+      fecr_dir="${1}"
+      fecr_keep="true"
+      ;;
+    --keep)
+      fecr_keep="true"
+      ;;
+    --skip)
+      fecr_skip="true"
+      ;;
+    -h|--help)
+      print_usage
+      exit 0
+      ;;
+    *)
+      print_usage
+      exit 1
+      ;;
+  esac
+  shift
+done
+
+trap fecr_clean_up INT
+trap fecr_clean_up EXIT
+
+if [[ -n "${fecr_verbose}" ]]; then
+  set -x
+fi
+
+. "${fecr_script_path}/ferrochrome-precondition-checker.sh"
+
+resolved_activities=$(adb shell pm query-activities --components -a ${ACTION_NAME})
+
+if [[ "$(echo ${resolved_activities} | wc -l)" != "1" ]]; then
+  >&2 echo "Multiple VM launchers exists"
+  exit 1
+fi
+
+pkg_name=$(dirname ${resolved_activities})
+
+adb shell pm grant ${pkg_name} android.permission.USE_CUSTOM_VIRTUAL_MACHINE > /dev/null
+adb shell pm clear ${pkg_name} > /dev/null
+
+if [[ -z "${fecr_skip}" ]]; then
+  if [[ -z "${fecr_dir}" ]]; then
+    # Download fecr image archive, and extract necessary files
+    # DISCLAIMER: Image is too large (1.5G+ for compressed, 6.5G+ for uncompressed), so can't submit.
+    fecr_dir=$(mktemp -d)
+
+    echo "Downloading & extracting ferrochrome image to ${fecr_dir}"
+    fecr_version=${fecr_version:-${FECR_DEFAULT_VERSION}}
+    curl ${FECR_GS_URL}/${fecr_version}/chromiumos_test_image.tar.xz | tar xfJ - -C ${fecr_dir}
+  fi
+
+  echo "Pushing ferrochrome image to ${FECR_DEVICE_DIR}"
+  adb shell mkdir -p ${FECR_DEVICE_DIR} > /dev/null || true
+  adb push ${fecr_dir}/chromiumos_test_image.bin ${FECR_DEVICE_DIR}
+  adb push ${fecr_script_path}/assets/vm_config.json ${FECR_CONFIG_PATH}
+fi
+
+echo "Starting ferrochrome"
+adb shell am start-activity -a ${ACTION_NAME} > /dev/null
+
+log_path="/data/data/${pkg_name}/files/console.log"
+fecr_start_time=${EPOCHSECONDS}
+
+while [[ $((EPOCHSECONDS - fecr_start_time)) -lt ${FECR_BOOT_TIMEOUT} ]]; do
+  adb shell grep -sF \""${FECR_BOOT_COMPLETED_LOG}"\" "${log_path}" && exit 0
+  sleep 10
+done
+
+>&2 echo "Ferrochrome failed to boot. Dumping console log"
+>&2 adb shell cat ${log_path}
+
+exit 1
diff --git a/tests/helper/Android.bp b/tests/helper/Android.bp
index 41d1ba2..1c38d12 100644
--- a/tests/helper/Android.bp
+++ b/tests/helper/Android.bp
@@ -33,4 +33,5 @@
     ],
     host_supported: true,
     device_supported: false,
+    sdk_version: "test_current",
 }
diff --git a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
index 6040531..026cf3f 100644
--- a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
+++ b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
@@ -20,8 +20,8 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
 
-import static org.junit.Assume.assumeTrue;
 import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
 
 import android.app.Instrumentation;
 import android.app.UiAutomation;
@@ -191,6 +191,9 @@
             assume().withMessage("Skip where protected VMs aren't supported")
                     .that(capabilities & VirtualMachineManager.CAPABILITY_PROTECTED_VM)
                     .isNotEqualTo(0);
+            assume().withMessage("Testing protected VMs on GSI isn't supported. b/272443823")
+                    .that(isGsi())
+                    .isFalse();
         } else {
             assume().withMessage("Skip where VMs aren't supported")
                     .that(capabilities & VirtualMachineManager.CAPABILITY_NON_PROTECTED_VM)
@@ -212,14 +215,19 @@
                 .that(mCtx.getPackageManager().hasSystemFeature(FEATURE_VIRTUALIZATION_FRAMEWORK))
                 .isTrue();
         int vendorApiLevel = getVendorApiLevel();
-        boolean isGsi = new File("/system/system_ext/etc/init/init.gsi.rc").exists();
+        boolean isGsi = isGsi();
+        Log.i(TAG, "isGsi = " + isGsi + ", vendor api level = " + vendorApiLevel);
         assume().withMessage("GSI with vendor API level < 202404 may not support AVF")
                 .that(isGsi && vendorApiLevel < 202404)
                 .isFalse();
     }
 
+    protected boolean isGsi() {
+        return new File("/system/system_ext/etc/init/init.gsi.rc").exists();
+    }
+
     protected static int getVendorApiLevel() {
-        return SystemProperties.getInt("ro.vendor.api_level", 0);
+        return SystemProperties.getInt("ro.board.api_level", 0);
     }
 
     protected void assumeSupportedDevice() {
@@ -548,6 +556,7 @@
         public int mFileMode;
         public int mMountFlags;
         public String mConsoleInput;
+        public byte[] mInstanceSecret;
 
         public void assertNoException() {
             if (mException != null) {
diff --git a/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java b/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java
index 41ddd48..1fc0f92 100644
--- a/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java
+++ b/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java
@@ -55,6 +55,7 @@
     protected static final String LOG_PATH = TEST_ROOT + "log.txt";
     protected static final String CONSOLE_PATH = TEST_ROOT + "console.txt";
     protected static final String TRADEFED_CONSOLE_PATH = TRADEFED_TEST_ROOT + "console.txt";
+    protected static final String TRADEFED_LOG_PATH = TRADEFED_TEST_ROOT + "log.txt";
     private static final int TEST_VM_ADB_PORT = 8000;
     private static final String MICRODROID_SERIAL = "localhost:" + TEST_VM_ADB_PORT;
     private static final String INSTANCE_IMG = "instance.img";
@@ -139,7 +140,7 @@
         assumeTrue("Requires VM support", testDevice.supportsMicrodroid());
 
         CommandRunner android = new CommandRunner(androidDevice);
-        long vendorApiLevel = androidDevice.getIntProperty("ro.vendor.api_level", 0);
+        long vendorApiLevel = androidDevice.getIntProperty("ro.board.api_level", 0);
         boolean isGsi =
                 android.runForResult("[ -e /system/system_ext/etc/init/init.gsi.rc ]").getStatus()
                         == CommandStatus.SUCCESS;
@@ -281,4 +282,8 @@
                 .map(os -> os.replaceFirst("^microdroid_gki-", ""))
                 .collect(Collectors.toList());
     }
+
+    protected boolean isPkvmHypervisor() throws DeviceNotAvailableException {
+        return getDevice().getProperty("ro.boot.hypervisor.version").equals("kvm.arm-protected");
+    }
 }
diff --git a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
index f424ce0..80d1fc6 100644
--- a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
+++ b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
@@ -809,8 +809,10 @@
         // Check VmCreationRequested atom
         AtomsProto.VmCreationRequested atomVmCreationRequested =
                 data.get(0).getAtom().getVmCreationRequested();
-        assertThat(atomVmCreationRequested.getHypervisor())
-                .isEqualTo(AtomsProto.VmCreationRequested.Hypervisor.PKVM);
+        if (isPkvmHypervisor()) {
+            assertThat(atomVmCreationRequested.getHypervisor())
+                    .isEqualTo(AtomsProto.VmCreationRequested.Hypervisor.PKVM);
+        }
         assertThat(atomVmCreationRequested.getIsProtected()).isEqualTo(mProtectedVm);
         assertThat(atomVmCreationRequested.getCreationSucceeded()).isTrue();
         assertThat(atomVmCreationRequested.getBinderExceptionCode()).isEqualTo(0);
@@ -832,7 +834,11 @@
         assertThat(atomVmExited.getDeathReason()).isEqualTo(AtomsProto.VmExited.DeathReason.KILLED);
         assertThat(atomVmExited.getExitSignal()).isEqualTo(9);
         // In CPU & memory related fields, check whether positive values are collected or not.
-        assertThat(atomVmExited.getGuestTimeMillis()).isGreaterThan(0);
+        if (isPkvmHypervisor()) {
+            // Guest Time may not be updated on other hypervisors.
+            // Checking only if the hypervisor is PKVM.
+            assertThat(atomVmExited.getGuestTimeMillis()).isGreaterThan(0);
+        }
         assertThat(atomVmExited.getRssVmKb()).isGreaterThan(0);
         assertThat(atomVmExited.getRssCrosvmKb()).isGreaterThan(0);
 
@@ -871,10 +877,13 @@
         assertWithMessage("Incorrect ABI list").that(abis).hasLength(1);
 
         // Check that no denials have happened so far
-        String logText =
-                getDevice().pullFileContents(CONSOLE_PATH) + getDevice().pullFileContents(LOG_PATH);
+        String consoleText = getDevice().pullFileContents(TRADEFED_CONSOLE_PATH);
+        assertWithMessage("Console output shouldn't be empty").that(consoleText).isNotEmpty();
+        String logText = getDevice().pullFileContents(TRADEFED_LOG_PATH);
+        assertWithMessage("Log output shouldn't be empty").that(logText).isNotEmpty();
+
         assertWithMessage("Unexpected denials during VM boot")
-                .that(logText)
+                .that(consoleText + logText)
                 .doesNotContainMatch("avc:\\s+denied");
 
         assertThat(getDeviceNumCpus(microdroid)).isEqualTo(getDeviceNumCpus(android));
@@ -1029,7 +1038,7 @@
         assumeFalse("Unlocked devices may have AVF debug policy", lockProp.equals("orange"));
 
         // Test that AVF debug policy doesn't exist.
-        boolean hasDebugPolicy = device.doesFileExist("/proc/device-tree/avf");
+        boolean hasDebugPolicy = device.doesFileExist("/proc/device-tree/avf/guest");
         assertThat(hasDebugPolicy).isFalse();
     }
 
@@ -1171,6 +1180,40 @@
         }
     }
 
+    @Test
+    public void testHugePages() throws Exception {
+        ITestDevice device = getDevice();
+        boolean disableRoot = !device.isAdbRoot();
+        CommandRunner android = new CommandRunner(device);
+
+        final String SHMEM_ENABLED_PATH = "/sys/kernel/mm/transparent_hugepage/shmem_enabled";
+        String thpShmemStr = android.run("cat", SHMEM_ENABLED_PATH);
+
+        assumeFalse("shmem already enabled, skip", thpShmemStr.contains("[advise]"));
+        assumeTrue("Unsupported shmem, skip", thpShmemStr.contains("[never]"));
+
+        device.enableAdbRoot();
+        assumeTrue("adb root is not enabled", device.isAdbRoot());
+        android.run("echo advise > " + SHMEM_ENABLED_PATH);
+
+        final String configPath = "assets/vm_config.json";
+        mMicrodroidDevice =
+                MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
+                        .debugLevel("full")
+                        .memoryMib(minMemorySize())
+                        .cpuTopology("match_host")
+                        .protectedVm(mProtectedVm)
+                        .gki(mGki)
+                        .hugePages(true)
+                        .build(getAndroidDevice());
+        mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
+
+        android.run("echo never >" + SHMEM_ENABLED_PATH);
+        if (disableRoot) {
+            device.disableAdbRoot();
+        }
+    }
+
     @Before
     public void setUp() throws Exception {
         assumeDeviceIsCapable(getDevice());
diff --git a/tests/pvmfw/helper/Android.bp b/tests/pvmfw/helper/Android.bp
index 90ca03e..a75f034 100644
--- a/tests/pvmfw/helper/Android.bp
+++ b/tests/pvmfw/helper/Android.bp
@@ -5,7 +5,7 @@
 java_library_host {
     name: "PvmfwHostTestHelper",
     srcs: ["java/**/*.java"],
-    libs: [
+    static_libs: [
         "androidx.annotation_annotation",
         "truth",
     ],
diff --git a/tests/pvmfw/tools/PvmfwTool.java b/tests/pvmfw/tools/PvmfwTool.java
index e150ec4..9f0cb42 100644
--- a/tests/pvmfw/tools/PvmfwTool.java
+++ b/tests/pvmfw/tools/PvmfwTool.java
@@ -25,10 +25,10 @@
 public class PvmfwTool {
     public static void printUsage() {
         System.out.println("pvmfw-tool: Appends pvmfw.bin and config payloads.");
-        System.out.println("            Requires BCC and VM reference DT.");
-        System.out.println("            VM DTBO and Debug policy can optionally be specified");
+        System.out.println("            Requires BCC. VM Reference DT, VM DTBO, and Debug policy");
+        System.out.println("            can optionally be specified");
         System.out.println(
-                "Usage: pvmfw-tool <out> <pvmfw.bin> <bcc.dat> <VM reference DT> [VM DTBO] [debug"
+                "Usage: pvmfw-tool <out> <pvmfw.bin> <bcc.dat> [VM reference DT] [VM DTBO] [debug"
                         + " policy]");
     }
 
@@ -41,10 +41,13 @@
         File out = new File(args[0]);
         File pvmfwBin = new File(args[1]);
         File bccData = new File(args[2]);
-        File vmReferenceDt = new File(args[3]);
 
+        File vmReferenceDt = null;
         File vmDtbo = null;
         File dp = null;
+        if (args.length > 3) {
+            vmReferenceDt = new File(args[3]);
+        }
         if (args.length > 4) {
             vmDtbo = new File(args[4]);
         }
@@ -53,12 +56,18 @@
         }
 
         try {
-            Pvmfw pvmfw =
+            Pvmfw.Builder builder =
                     new Pvmfw.Builder(pvmfwBin, bccData)
                             .setVmReferenceDt(vmReferenceDt)
                             .setDebugPolicyOverlay(dp)
-                            .setVmDtbo(vmDtbo)
-                            .build();
+                            .setVmDtbo(vmDtbo);
+            if (vmReferenceDt == null) {
+                builder.setVersion(1, 1);
+            } else {
+                builder.setVersion(1, 2);
+            }
+
+            Pvmfw pvmfw = builder.build();
             pvmfw.serialize(out);
         } catch (IOException e) {
             e.printStackTrace();
diff --git a/tests/testapk/Android.bp b/tests/testapk/Android.bp
index 471aea7..e32ff88 100644
--- a/tests/testapk/Android.bp
+++ b/tests/testapk/Android.bp
@@ -55,6 +55,7 @@
         "MicrodroidExitNativeLib",
         "MicrodroidPrivateLinkingNativeLib",
         "MicrodroidCrashNativeLib",
+        "libmicrodroid_testlib_rust",
         "libvm_attestation_test_payload",
     ],
     min_sdk_version: "33",
@@ -166,3 +167,22 @@
     header_libs: ["vm_payload_headers"],
     stl: "libc++_static",
 }
+
+// A payload written in Rust, using the Rust wrapper for the VM payload API.
+rust_ffi_shared {
+    name: "libmicrodroid_testlib_rust",
+    crate_name: "microdroid_testlib_rust",
+    defaults: ["avf_build_flags_rust"],
+    prefer_rlib: true,
+    srcs: ["src/native/testbinary.rs"],
+    compile_multilib: "both",
+    rustlibs: [
+        "com.android.microdroid.testservice-rust",
+        "libandroid_logger",
+        "libanyhow",
+        "libavflog",
+        "libcstr",
+        "liblog_rust",
+        "libvm_payload_rs",
+    ],
+}
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 4ffef3c..c94f171 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -28,8 +28,6 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.TruthJUnit.assume;
-import com.android.virt.vm_attestation.testservice.IAttestationService.AttestationStatus;
-import com.android.virt.vm_attestation.testservice.IAttestationService.SigningResult;
 
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
@@ -39,6 +37,7 @@
 import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
 import static java.util.stream.Collectors.toList;
 
+import android.app.ActivityManager;
 import android.app.Instrumentation;
 import android.app.UiAutomation;
 import android.content.ComponentName;
@@ -70,8 +69,15 @@
 import com.android.microdroid.testservice.IAppCallback;
 import com.android.microdroid.testservice.ITestService;
 import com.android.microdroid.testservice.IVmCallback;
+import com.android.virt.vm_attestation.testservice.IAttestationService.AttestationStatus;
+import com.android.virt.vm_attestation.testservice.IAttestationService.SigningResult;
 import com.android.virt.vm_attestation.util.X509Utils;
 
+import co.nstant.in.cbor.CborDecoder;
+import co.nstant.in.cbor.model.Array;
+import co.nstant.in.cbor.model.DataItem;
+import co.nstant.in.cbor.model.MajorType;
+
 import com.google.common.base.Strings;
 import com.google.common.truth.BooleanSubject;
 
@@ -113,17 +119,13 @@
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.stream.Stream;
 
-import co.nstant.in.cbor.CborDecoder;
-import co.nstant.in.cbor.model.Array;
-import co.nstant.in.cbor.model.DataItem;
-import co.nstant.in.cbor.model.MajorType;
-
 @RunWith(Parameterized.class)
 public class MicrodroidTests extends MicrodroidDeviceTestBase {
     private static final String TAG = "MicrodroidTests";
     private static final String TEST_APP_PACKAGE_NAME = "com.android.microdroid.test";
     private static final String VM_ATTESTATION_PAYLOAD_PATH = "libvm_attestation_test_payload.so";
     private static final String VM_ATTESTATION_MESSAGE = "Hello RKP from AVF!";
+    private static final int ENCRYPTED_STORAGE_BYTES = 4_000_000;
 
     @Rule public Timeout globalTimeout = Timeout.seconds(300);
 
@@ -196,6 +198,7 @@
                             tr.mSublibRunProp = ts.readProperty("debug.microdroid.app.sublib.run");
                             tr.mApkContentsPath = ts.getApkContentsPath();
                             tr.mEncryptedStoragePath = ts.getEncryptedStoragePath();
+                            tr.mInstanceSecret = ts.insecurelyExposeVmInstanceSecret();
                         });
         testResults.assertNoException();
         assertThat(testResults.mAddInteger).isEqualTo(123 + 456);
@@ -203,6 +206,7 @@
         assertThat(testResults.mSublibRunProp).isEqualTo("true");
         assertThat(testResults.mApkContentsPath).isEqualTo("/mnt/apk");
         assertThat(testResults.mEncryptedStoragePath).isEqualTo("");
+        assertThat(testResults.mInstanceSecret).hasLength(32);
     }
 
     @Test
@@ -571,7 +575,7 @@
         assertThat(minimal.isProtectedVm()).isEqualTo(isProtectedVm());
         assertThat(minimal.isEncryptedStorageEnabled()).isFalse();
         assertThat(minimal.getEncryptedStorageBytes()).isEqualTo(0);
-        assertThat(minimal.isVmOutputCaptured()).isEqualTo(false);
+        assertThat(minimal.isVmOutputCaptured()).isFalse();
         assertThat(minimal.getOs()).isEqualTo("microdroid");
 
         // Maximal has everything that can be set to some non-default value. (And has different
@@ -603,7 +607,7 @@
         assertThat(maximal.isProtectedVm()).isEqualTo(isProtectedVm());
         assertThat(maximal.isEncryptedStorageEnabled()).isTrue();
         assertThat(maximal.getEncryptedStorageBytes()).isEqualTo(1_000_000);
-        assertThat(maximal.isVmOutputCaptured()).isEqualTo(true);
+        assertThat(maximal.isVmOutputCaptured()).isTrue();
         assertThat(maximal.getOs()).isEqualTo("microdroid_gki-android14-6.1");
 
         assertThat(minimal.isCompatibleWith(maximal)).isFalse();
@@ -1583,7 +1587,7 @@
                 newVmConfigBuilderWithPayloadBinary("MicrodroidTestNativeLib.so")
                         .setDebugLevel(DEBUG_LEVEL_FULL);
         if (encryptedStoreEnabled) {
-            builder.setEncryptedStorageBytes(4_000_000);
+            builder.setEncryptedStorageBytes(ENCRYPTED_STORAGE_BYTES);
         }
         VirtualMachineConfig config = builder.build();
         String vmNameOrig = "test_vm_orig";
@@ -1637,7 +1641,7 @@
         VirtualMachineConfig config =
                 newVmConfigBuilderWithPayloadBinary("MicrodroidTestNativeLib.so")
                         .setMemoryBytes(minMemoryRequired())
-                        .setEncryptedStorageBytes(4_000_000)
+                        .setEncryptedStorageBytes(ENCRYPTED_STORAGE_BYTES)
                         .setDebugLevel(DEBUG_LEVEL_FULL)
                         .build();
         VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config);
@@ -1664,7 +1668,7 @@
         VirtualMachineConfig config =
                 newVmConfigBuilderWithPayloadBinary("MicrodroidTestNativeLib.so")
                         .setMemoryBytes(minMemoryRequired())
-                        .setEncryptedStorageBytes(4_000_000)
+                        .setEncryptedStorageBytes(ENCRYPTED_STORAGE_BYTES)
                         .setDebugLevel(DEBUG_LEVEL_FULL)
                         .build();
 
@@ -1771,7 +1775,7 @@
         VirtualMachineConfig config =
                 newVmConfigBuilderWithPayloadBinary("MicrodroidTestNativeLib.so")
                         .setMemoryBytes(minMemoryRequired())
-                        .setEncryptedStorageBytes(4_000_000)
+                        .setEncryptedStorageBytes(ENCRYPTED_STORAGE_BYTES)
                         .setDebugLevel(DEBUG_LEVEL_FULL)
                         .build();
         VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_a", config);
@@ -2107,12 +2111,9 @@
         IVmShareTestService service = connection.waitForService();
         assertWithMessage("Timed out connecting to " + serviceIntent).that(service).isNotNull();
 
+
         try {
-            // Send the VM descriptor to the other app. When received, it will reconstruct the VM
-            // from the descriptor, start it, connect to the ITestService in it, creates a "proxy"
-            // ITestService binder that delegates all the calls to the VM, and share it with this
-            // app. It will allow us to verify assertions on the running VM in the other app.
-            ITestService testServiceProxy = service.startVm(vmDesc);
+            ITestService testServiceProxy = transferAndStartVm(service, vmDesc, "vm_to_share");
 
             int result = testServiceProxy.addInteger(37, 73);
             assertThat(result).isEqualTo(110);
@@ -2163,12 +2164,7 @@
         assertWithMessage("Timed out connecting to " + serviceIntent).that(service).isNotNull();
 
         try {
-            // Send the VM descriptor to the other app. When received, it will reconstruct the VM
-            // from the descriptor, start it, connect to the ITestService in it, creates a "proxy"
-            // ITestService binder that delegates all the calls to the VM, and share it with this
-            // app. It will allow us to verify assertions on the running VM in the other app.
-            ITestService testServiceProxy = service.startVm(vmDesc);
-
+            ITestService testServiceProxy = transferAndStartVm(service, vmDesc, "vm_to_share");
             String result = testServiceProxy.readFromFile("/mnt/encryptedstore/private.key");
             assertThat(result).isEqualTo(EXAMPLE_STRING);
         } finally {
@@ -2176,6 +2172,25 @@
         }
     }
 
+    private ITestService transferAndStartVm(
+            IVmShareTestService service, VirtualMachineDescriptor vmDesc, String vmName)
+            throws Exception {
+        // Send the VM descriptor to the other app. When received, it will reconstruct the VM
+        // from the descriptor.
+        service.importVm(vmDesc);
+
+        // Now that the VM has been imported, we should be free to delete our copy (this is
+        // what we recommend for VM transfer).
+        getVirtualMachineManager().delete(vmName);
+
+        // Ask the other app to start the imported VM, connect to the ITestService in it, create
+        // a "proxy" ITestService binder that delegates all the calls to the VM, and share it
+        // with this app. It will allow us to verify assertions on the running VM in the other
+        // app.
+        ITestService testServiceProxy = service.startVm();
+        return testServiceProxy;
+    }
+
     @Test
     @CddTest(requirements = {"9.17/C-1-5"})
     public void testFileUnderBinHasExecutePermission() throws Exception {
@@ -2252,7 +2267,7 @@
         VirtualMachineConfig vmConfig =
                 newVmConfigBuilderWithPayloadBinary("MicrodroidTestNativeLib.so")
                         .setDebugLevel(DEBUG_LEVEL_FULL)
-                        .setEncryptedStorageBytes(4_000_000)
+                        .setEncryptedStorageBytes(ENCRYPTED_STORAGE_BYTES)
                         .build();
         VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_encstore_no_exec", vmConfig);
 
@@ -2289,6 +2304,63 @@
         }
     }
 
+    @Test
+    public void createAndRunRustVm() throws Exception {
+        // This test is here mostly to exercise the Rust wrapper around the VM Payload API.
+        // We're testing the same functionality as in other tests, the only difference is
+        // that the payload is written in Rust.
+
+        assumeSupportedDevice();
+
+        VirtualMachineConfig config =
+                newVmConfigBuilderWithPayloadBinary("libmicrodroid_testlib_rust.so")
+                        .setMemoryBytes(minMemoryRequired())
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .build();
+        VirtualMachine vm = forceCreateNewVirtualMachine("rust_vm", config);
+
+        TestResults testResults =
+                runVmTestService(
+                        TAG,
+                        vm,
+                        (ts, tr) -> {
+                            tr.mAddInteger = ts.addInteger(37, 73);
+                            tr.mApkContentsPath = ts.getApkContentsPath();
+                            tr.mEncryptedStoragePath = ts.getEncryptedStoragePath();
+                            tr.mInstanceSecret = ts.insecurelyExposeVmInstanceSecret();
+                        });
+        testResults.assertNoException();
+        assertThat(testResults.mAddInteger).isEqualTo(37 + 73);
+        assertThat(testResults.mApkContentsPath).isEqualTo("/mnt/apk");
+        assertThat(testResults.mEncryptedStoragePath).isEqualTo("");
+        assertThat(testResults.mInstanceSecret).hasLength(32);
+    }
+
+    @Test
+    public void createAndRunRustVmWithEncryptedStorage() throws Exception {
+        // This test is here mostly to exercise the Rust wrapper around the VM Payload API.
+        // We're testing the same functionality as in other tests, the only difference is
+        // that the payload is written in Rust.
+
+        assumeSupportedDevice();
+
+        VirtualMachineConfig config =
+                newVmConfigBuilderWithPayloadBinary("libmicrodroid_testlib_rust.so")
+                        .setMemoryBytes(minMemoryRequired())
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .setEncryptedStorageBytes(ENCRYPTED_STORAGE_BYTES)
+                        .build();
+        VirtualMachine vm = forceCreateNewVirtualMachine("rust_vm", config);
+
+        TestResults testResults =
+                runVmTestService(
+                        TAG,
+                        vm,
+                        (ts, tr) -> tr.mEncryptedStoragePath = ts.getEncryptedStoragePath());
+        testResults.assertNoException();
+        assertThat(testResults.mEncryptedStoragePath).isEqualTo("/mnt/encryptedstore");
+    }
+
     private VirtualMachineConfig buildVmConfigWithVendor(File vendorDiskImage) throws Exception {
         return buildVmConfigWithVendor(vendorDiskImage, "MicrodroidTestNativeLib.so");
     }
@@ -2441,6 +2513,52 @@
         }
     }
 
+    @Test
+    public void concurrentVms() throws Exception {
+        final long vmSize = minMemoryRequired();
+        final int numVMs = 8;
+        final long availableMem = getAvailableMemory();
+
+        // Let's not use more than half of the available memory
+        assume().withMessage("Available memory (" + availableMem + " bytes) too small")
+                .that((numVMs * vmSize) <= (availableMem / 2))
+                .isTrue();
+
+        VirtualMachine[] vms = new VirtualMachine[numVMs];
+        try {
+            for (int i = 0; i < numVMs; i++) {
+                VirtualMachineConfig config =
+                        newVmConfigBuilderWithPayloadBinary("MicrodroidIdleNativeLib.so")
+                                .setDebugLevel(DEBUG_LEVEL_NONE)
+                                .setMemoryBytes(vmSize)
+                                .build();
+
+                vms[i] = forceCreateNewVirtualMachine("test_concurrent_vms_" + i, config);
+                vms[i].run();
+            }
+
+            for (VirtualMachine vm : vms) {
+                assertThat(vm.getStatus()).isEqualTo(VirtualMachine.STATUS_RUNNING);
+            }
+
+        } finally {
+            // Ensure that VMs are all stopped. Otherwise we may try to reuse some of these for
+            // another run of this test with different parameters.
+            for (VirtualMachine vm : vms) {
+                if (vm != null) {
+                    vm.close();
+                }
+            }
+        }
+    }
+
+    private long getAvailableMemory() {
+        ActivityManager am = getContext().getSystemService(ActivityManager.class);
+        ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
+        am.getMemoryInfo(memoryInfo);
+        return memoryInfo.availMem;
+    }
+
     private VirtualMachineDescriptor toParcelFromParcel(VirtualMachineDescriptor descriptor) {
         Parcel parcel = Parcel.obtain();
         descriptor.writeToParcel(parcel, 0);
diff --git a/tests/testapk/src/native/testbinary.rs b/tests/testapk/src/native/testbinary.rs
new file mode 100644
index 0000000..85b411e
--- /dev/null
+++ b/tests/testapk/src/native/testbinary.rs
@@ -0,0 +1,138 @@
+/*
+ * 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.
+ */
+
+//! A VM payload that exists to allow testing of the Rust wrapper for the VM payload APIs.
+
+use anyhow::Result;
+use com_android_microdroid_testservice::{
+    aidl::com::android::microdroid::testservice::{
+        IAppCallback::IAppCallback,
+        ITestService::{BnTestService, ITestService, PORT},
+    },
+    binder::{BinderFeatures, ExceptionCode, Interface, Result as BinderResult, Status, Strong},
+};
+use cstr::cstr;
+use log::{error, info};
+use std::panic;
+use std::process::exit;
+use std::string::String;
+use std::vec::Vec;
+
+vm_payload::main!(main);
+
+// Entry point of the Service VM client.
+fn main() {
+    android_logger::init_once(
+        android_logger::Config::default()
+            .with_tag("microdroid_testlib_rust")
+            .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);
+        exit(1);
+    }
+}
+
+fn try_main() -> Result<()> {
+    info!("Welcome to the Rust test binary");
+
+    vm_payload::run_single_vsock_service(TestService::new_binder(), PORT.try_into()?)
+}
+
+struct TestService {}
+
+impl Interface for TestService {}
+
+impl TestService {
+    fn new_binder() -> Strong<dyn ITestService> {
+        BnTestService::new_binder(TestService {}, BinderFeatures::default())
+    }
+}
+
+impl ITestService for TestService {
+    fn quit(&self) -> BinderResult<()> {
+        exit(0)
+    }
+
+    fn addInteger(&self, a: i32, b: i32) -> BinderResult<i32> {
+        a.checked_add(b).ok_or_else(|| Status::new_exception(ExceptionCode::ILLEGAL_ARGUMENT, None))
+    }
+
+    fn getApkContentsPath(&self) -> BinderResult<String> {
+        Ok(vm_payload::apk_contents_path().to_string_lossy().to_string())
+    }
+
+    fn getEncryptedStoragePath(&self) -> BinderResult<String> {
+        Ok(vm_payload::encrypted_storage_path()
+            .map(|p| p.to_string_lossy().to_string())
+            .unwrap_or("".to_string()))
+    }
+
+    fn insecurelyExposeVmInstanceSecret(&self) -> BinderResult<Vec<u8>> {
+        let mut secret = vec![0u8; 32];
+        vm_payload::get_vm_instance_secret(b"identifier", secret.as_mut_slice());
+        Ok(secret)
+    }
+
+    // Everything below here is unimplemented. Implementations may be added as needed.
+
+    fn readProperty(&self, _: &str) -> BinderResult<String> {
+        unimplemented()
+    }
+    fn insecurelyExposeAttestationCdi(&self) -> BinderResult<Vec<u8>> {
+        unimplemented()
+    }
+    fn getBcc(&self) -> BinderResult<Vec<u8>> {
+        unimplemented()
+    }
+    fn runEchoReverseServer(&self) -> BinderResult<()> {
+        unimplemented()
+    }
+    fn getEffectiveCapabilities(&self) -> BinderResult<Vec<String>> {
+        unimplemented()
+    }
+    fn getUid(&self) -> BinderResult<i32> {
+        unimplemented()
+    }
+    fn writeToFile(&self, _: &str, _: &str) -> BinderResult<()> {
+        unimplemented()
+    }
+    fn readFromFile(&self, _: &str) -> BinderResult<String> {
+        unimplemented()
+    }
+    fn getFilePermissions(&self, _: &str) -> BinderResult<i32> {
+        unimplemented()
+    }
+    fn getMountFlags(&self, _: &str) -> BinderResult<i32> {
+        unimplemented()
+    }
+    fn requestCallback(&self, _: &Strong<dyn IAppCallback + 'static>) -> BinderResult<()> {
+        unimplemented()
+    }
+    fn readLineFromConsole(&self) -> BinderResult<String> {
+        unimplemented()
+    }
+}
+
+fn unimplemented<T>() -> BinderResult<T> {
+    let message = cstr!("Got a call to an unimplemented ITestService method in testbinary.rs");
+    error!("{message:?}");
+    Err(Status::new_exception(ExceptionCode::UNSUPPORTED_OPERATION, Some(message)))
+}
diff --git a/tests/vmshareapp/aidl/com/android/microdroid/test/vmshare/IVmShareTestService.aidl b/tests/vmshareapp/aidl/com/android/microdroid/test/vmshare/IVmShareTestService.aidl
index fe6ca43..ac59610 100644
--- a/tests/vmshareapp/aidl/com/android/microdroid/test/vmshare/IVmShareTestService.aidl
+++ b/tests/vmshareapp/aidl/com/android/microdroid/test/vmshare/IVmShareTestService.aidl
@@ -20,5 +20,7 @@
 
 /** {@hide} */
 interface IVmShareTestService {
-    ITestService startVm(in VirtualMachineDescriptor vmDesc);
+    void importVm(in VirtualMachineDescriptor vmDesc);
+
+    ITestService startVm();
 }
diff --git a/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java b/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java
index dc8908b..109486c 100644
--- a/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java
+++ b/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java
@@ -93,16 +93,19 @@
         }
     }
 
-    public ITestService startVm(VirtualMachineDescriptor vmDesc) throws Exception {
+    public void importVm(VirtualMachineDescriptor vmDesc) throws Exception {
         // Cleanup VM left from the previous test.
         deleteVm();
 
-        VirtualMachineManager vmm = getSystemService(VirtualMachineManager.class);
-
         // Add random uuid to make sure that different tests that bind to this service don't trip
         // over each other.
         String vmName = "imported_vm" + UUID.randomUUID();
 
+        VirtualMachineManager vmm = getSystemService(VirtualMachineManager.class);
+        mVirtualMachine = vmm.importFromDescriptor(vmName, vmDesc);
+    }
+
+    public ITestService startVm() throws Exception {
         final CountDownLatch latch = new CountDownLatch(1);
         VirtualMachineCallback callback =
                 new VirtualMachineCallback() {
@@ -134,10 +137,9 @@
                     }
                 };
 
-        mVirtualMachine = vmm.importFromDescriptor(vmName, vmDesc);
         mVirtualMachine.setCallback(getMainExecutor(), callback);
 
-        Log.i(TAG, "Starting VM " + vmName);
+        Log.i(TAG, "Starting VM " + mVirtualMachine.getName());
         mVirtualMachine.run();
         if (!latch.await(1, TimeUnit.MINUTES)) {
             throw new TimeoutException("Timed out starting VM");
@@ -155,10 +157,21 @@
     final class ServiceImpl extends IVmShareTestService.Stub {
 
         @Override
-        public ITestService startVm(VirtualMachineDescriptor vmDesc) {
+        public void importVm(VirtualMachineDescriptor vmDesc) {
+            Log.i(TAG, "importVm binder call received");
+            try {
+                VmShareServiceImpl.this.importVm(vmDesc);
+            } catch (Exception e) {
+                Log.e(TAG, "Failed to importVm", e);
+                throw new IllegalStateException("Failed to importVm", e);
+            }
+        }
+
+        @Override
+        public ITestService startVm() {
             Log.i(TAG, "startVm binder call received");
             try {
-                return VmShareServiceImpl.this.startVm(vmDesc);
+                return VmShareServiceImpl.this.startVm();
             } catch (Exception e) {
                 Log.e(TAG, "Failed to startVm", e);
                 throw new IllegalStateException("Failed to startVm", e);
diff --git a/virtualizationmanager/Android.bp b/virtualizationmanager/Android.bp
index d8f8209..d1ef4de 100644
--- a/virtualizationmanager/Android.bp
+++ b/virtualizationmanager/Android.bp
@@ -100,3 +100,21 @@
     ],
     test_suites: ["general-tests"],
 }
+
+cc_fuzz {
+    name: "virtualizationmanager_fuzzer",
+    defaults: ["service_fuzzer_defaults"],
+    srcs: ["fuzzer.cpp"],
+    static_libs: [
+        "android.system.virtualizationservice-ndk",
+        "libbase",
+    ],
+    shared_libs: [
+        "libbinder_ndk",
+        "libbinder_rpc_unstable",
+        "liblog",
+    ],
+    fuzz_config: {
+        cc: ["android-kvm@google.com"],
+    },
+}
diff --git a/virtualizationmanager/fuzzer.cpp b/virtualizationmanager/fuzzer.cpp
new file mode 100644
index 0000000..6afea46
--- /dev/null
+++ b/virtualizationmanager/fuzzer.cpp
@@ -0,0 +1,106 @@
+/*
+ * 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.
+ */
+#include <aidl/android/system/virtualizationservice/IVirtualizationService.h>
+#include <android-base/file.h>
+#include <android-base/result.h>
+#include <android-base/unique_fd.h>
+#include <fuzzbinder/libbinder_ndk_driver.h>
+#include <fuzzer/FuzzedDataProvider.h>
+#include <unistd.h>
+
+#include <binder_rpc_unstable.hpp>
+#include <cstdlib>
+#include <iostream>
+
+using aidl::android::system::virtualizationservice::IVirtualizationService;
+using android::fuzzService;
+using android::base::ErrnoError;
+using android::base::Error;
+using android::base::Pipe;
+using android::base::Result;
+using android::base::Socketpair;
+using android::base::unique_fd;
+using ndk::SpAIBinder;
+
+static constexpr const char VIRTMGR_PATH[] = "/apex/com.android.virt/bin/virtmgr";
+static constexpr size_t VIRTMGR_THREADS = 2;
+
+Result<unique_fd> get_service_fd() {
+    unique_fd server_fd, client_fd;
+    if (!Socketpair(SOCK_STREAM, &server_fd, &client_fd)) {
+        return ErrnoError() << "Failed to create socketpair";
+    }
+
+    unique_fd wait_fd, ready_fd;
+    if (!Pipe(&wait_fd, &ready_fd, 0)) {
+        return ErrnoError() << "Failed to create pipe";
+    }
+
+    if (int pid = fork(); pid == 0) {
+        client_fd.reset();
+        wait_fd.reset();
+
+        auto server_fd_str = std::to_string(server_fd.get());
+        auto ready_fd_str = std::to_string(ready_fd.get());
+
+        if (execl(VIRTMGR_PATH, VIRTMGR_PATH, "--rpc-server-fd", server_fd_str.c_str(),
+                  "--ready-fd", ready_fd_str.c_str(), nullptr) == -1) {
+            return ErrnoError() << "Failed to execute virtmgr";
+        }
+    } else if (pid < 0) {
+        return ErrnoError() << "Failed to fork";
+    }
+
+    server_fd.reset();
+    ready_fd.reset();
+
+    char buf;
+    if (read(wait_fd.get(), &buf, sizeof(buf)) < 0) {
+        return ErrnoError() << "Failed to wait for VirtualizationService to be ready";
+    }
+
+    return client_fd;
+}
+
+Result<std::shared_ptr<IVirtualizationService>> connect_service(int fd) {
+    std::unique_ptr<ARpcSession, decltype(&ARpcSession_free)> session(ARpcSession_new(),
+                                                                      &ARpcSession_free);
+    ARpcSession_setFileDescriptorTransportMode(session.get(),
+                                               ARpcSession_FileDescriptorTransportMode::Unix);
+    ARpcSession_setMaxIncomingThreads(session.get(), VIRTMGR_THREADS);
+    ARpcSession_setMaxOutgoingConnections(session.get(), VIRTMGR_THREADS);
+    AIBinder* binder = ARpcSession_setupUnixDomainBootstrapClient(session.get(), fd);
+    if (binder == nullptr) {
+        return Error() << "Failed to connect to VirtualizationService";
+    }
+    return IVirtualizationService::fromBinder(SpAIBinder{binder});
+}
+
+Result<void> inner_fuzz(const uint8_t* data, size_t size) {
+    unique_fd fd = OR_RETURN(get_service_fd());
+    std::shared_ptr<IVirtualizationService> service = OR_RETURN(connect_service(fd.get()));
+    fuzzService(service->asBinder().get(), FuzzedDataProvider(data, size));
+
+    return {};
+}
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+    if (auto ret = inner_fuzz(data, size); !ret.ok()) {
+        std::cerr << "connecting to service failed: " << ret.error() << std::endl;
+        abort();
+    }
+    return 0;
+}
diff --git a/virtualizationmanager/src/aidl.rs b/virtualizationmanager/src/aidl.rs
index aeee6f7..9df376a 100644
--- a/virtualizationmanager/src/aidl.rs
+++ b/virtualizationmanager/src/aidl.rs
@@ -17,7 +17,7 @@
 use crate::{get_calling_pid, get_calling_uid, get_this_pid};
 use crate::atom::{write_vm_booted_stats, write_vm_creation_stats};
 use crate::composite::make_composite_image;
-use crate::crosvm::{CrosvmConfig, DiskFile, DisplayConfig, InputDeviceOption, PayloadState, VmContext, VmInstance, VmState};
+use crate::crosvm::{CrosvmConfig, DiskFile, DisplayConfig, GpuConfig, InputDeviceOption, PayloadState, VmContext, VmInstance, VmState};
 use crate::debug_config::DebugConfig;
 use crate::dt_overlay::{create_device_tree_overlay, VM_DT_OVERLAY_MAX_SIZE, VM_DT_OVERLAY_PATH};
 use crate::payload::{add_microdroid_payload_images, add_microdroid_system_images, add_microdroid_vendor_image};
@@ -401,69 +401,10 @@
             check_gdb_allowed(config)?;
         }
 
-        // Currently, VirtMgr adds the host copy of reference DT & untrusted properties
-        // (e.g. instance-id)
-        let host_ref_dt = Path::new(VM_REFERENCE_DT_ON_HOST_PATH);
-        let host_ref_dt = if host_ref_dt.exists()
-            && read_dir(host_ref_dt).or_service_specific_exception(-1)?.next().is_some()
-        {
-            Some(host_ref_dt)
-        } else {
-            warn!("VM reference DT doesn't exist in host DT");
-            None
-        };
-
-        let vendor_hashtree_digest = extract_vendor_hashtree_digest(config)
-            .context("Failed to extract vendor hashtree digest")
-            .or_service_specific_exception(-1)?;
-
-        let trusted_props = if let Some(ref vendor_hashtree_digest) = vendor_hashtree_digest {
-            info!(
-                "Passing vendor hashtree digest to pvmfw. This will be rejected if it doesn't \
-                match the trusted digest in the pvmfw config, causing the VM to fail to start."
-            );
-            vec![(
-                cstr!("vendor_hashtree_descriptor_root_digest"),
-                vendor_hashtree_digest.as_slice(),
-            )]
-        } else {
-            vec![]
-        };
-
-        let instance_id;
-        let mut untrusted_props = Vec::with_capacity(2);
-        if cfg!(llpvm_changes) {
-            instance_id = extract_instance_id(config);
-            untrusted_props.push((cstr!("instance-id"), &instance_id[..]));
-            let want_updatable = extract_want_updatable(config);
-            if want_updatable && is_secretkeeper_supported() {
-                // Let guest know that it can defer rollback protection to Secretkeeper by setting
-                // an empty property in untrusted node in DT. This enables Updatable VMs.
-                untrusted_props.push((cstr!("defer-rollback-protection"), &[]))
-            }
-        }
-
-        let device_tree_overlay =
-            if host_ref_dt.is_some() || !untrusted_props.is_empty() || !trusted_props.is_empty() {
-                let dt_output = temporary_directory.join(VM_DT_OVERLAY_PATH);
-                let mut data = [0_u8; VM_DT_OVERLAY_MAX_SIZE];
-                let fdt = create_device_tree_overlay(
-                    &mut data,
-                    host_ref_dt,
-                    &untrusted_props,
-                    &trusted_props,
-                )
-                .map_err(|e| anyhow!("Failed to create DT overlay, {e:?}"))
-                .or_service_specific_exception(-1)?;
-                fs::write(&dt_output, fdt.as_slice()).or_service_specific_exception(-1)?;
-                Some(File::open(dt_output).or_service_specific_exception(-1)?)
-            } else {
-                None
-            };
+        let device_tree_overlay = maybe_create_device_tree_overlay(config, &temporary_directory)?;
 
         let debug_config = DebugConfig::new(config);
-
-        let ramdump = if debug_config.is_ramdump_needed() {
+        let ramdump = if !uses_gki_kernel(config) && debug_config.is_ramdump_needed() {
             Some(prepare_ramdump_file(&temporary_directory)?)
         } else {
             None
@@ -594,6 +535,16 @@
         } else {
             None
         };
+        let gpu_config = if cfg!(paravirtualized_devices) {
+            config
+                .gpuConfig
+                .as_ref()
+                .map(GpuConfig::new)
+                .transpose()
+                .or_binder_exception(ExceptionCode::ILLEGAL_ARGUMENT)?
+        } else {
+            None
+        };
 
         let input_device_options = if cfg!(paravirtualized_devices) {
             config
@@ -607,16 +558,25 @@
         };
 
         // Create TAP network interface if the VM supports network.
-        let _tap_fd = if cfg!(network) && config.networkSupported {
+        let tap = if cfg!(network) && config.networkSupported {
             if *is_protected {
                 return Err(anyhow!("Network feature is not supported for pVM yet"))
                     .with_log()
                     .or_binder_exception(ExceptionCode::UNSUPPORTED_OPERATION)?;
             }
-            Some(GLOBAL_SERVICE.createTapInterface(&get_this_pid().to_string())?)
+            Some(File::from(
+                GLOBAL_SERVICE
+                    .createTapInterface(&get_this_pid().to_string())?
+                    .as_ref()
+                    .try_clone()
+                    .context("Failed to get TAP interface from ParcelFileDescriptor")
+                    .or_binder_exception(ExceptionCode::BAD_PARCELABLE)?,
+            ))
         } else {
             None
         };
+        let virtio_snd_backend =
+            if cfg!(paravirtualized_devices) { Some(String::from("aaudio")) } else { None };
 
         // Actually start the VM.
         let crosvm_config = CrosvmConfig {
@@ -646,6 +606,11 @@
             display_config,
             input_device_options,
             hugepages: config.hugePages,
+            tap,
+            virtio_snd_backend,
+            console_input_device: config.consoleInputDevice.clone(),
+            boost_uclamp: config.boostUclamp,
+            gpu_config,
         };
         let instance = Arc::new(
             VmInstance::new(
@@ -720,6 +685,67 @@
     Err(anyhow!("No hashtree digest is extracted from microdroid vendor image"))
 }
 
+fn maybe_create_device_tree_overlay(
+    config: &VirtualMachineConfig,
+    temporary_directory: &Path,
+) -> binder::Result<Option<File>> {
+    // Currently, VirtMgr adds the host copy of reference DT & untrusted properties
+    // (e.g. instance-id)
+    let host_ref_dt = Path::new(VM_REFERENCE_DT_ON_HOST_PATH);
+    let host_ref_dt = if host_ref_dt.exists()
+        && read_dir(host_ref_dt).or_service_specific_exception(-1)?.next().is_some()
+    {
+        Some(host_ref_dt)
+    } else {
+        warn!("VM reference DT doesn't exist in host DT");
+        None
+    };
+
+    let vendor_hashtree_digest = extract_vendor_hashtree_digest(config)
+        .context("Failed to extract vendor hashtree digest")
+        .or_service_specific_exception(-1)?;
+
+    let trusted_props = if let Some(ref vendor_hashtree_digest) = vendor_hashtree_digest {
+        info!(
+            "Passing vendor hashtree digest to pvmfw. This will be rejected if it doesn't \
+                match the trusted digest in the pvmfw config, causing the VM to fail to start."
+        );
+        vec![(cstr!("vendor_hashtree_descriptor_root_digest"), vendor_hashtree_digest.as_slice())]
+    } else {
+        vec![]
+    };
+
+    let instance_id;
+    let mut untrusted_props = Vec::with_capacity(2);
+    if cfg!(llpvm_changes) {
+        instance_id = extract_instance_id(config);
+        untrusted_props.push((cstr!("instance-id"), &instance_id[..]));
+        let want_updatable = extract_want_updatable(config);
+        if want_updatable && is_secretkeeper_supported() {
+            // Let guest know that it can defer rollback protection to Secretkeeper by setting
+            // an empty property in untrusted node in DT. This enables Updatable VMs.
+            untrusted_props.push((cstr!("defer-rollback-protection"), &[]))
+        }
+    }
+
+    let device_tree_overlay = if host_ref_dt.is_some()
+        || !untrusted_props.is_empty()
+        || !trusted_props.is_empty()
+    {
+        let dt_output = temporary_directory.join(VM_DT_OVERLAY_PATH);
+        let mut data = [0_u8; VM_DT_OVERLAY_MAX_SIZE];
+        let fdt =
+            create_device_tree_overlay(&mut data, host_ref_dt, &untrusted_props, &trusted_props)
+                .map_err(|e| anyhow!("Failed to create DT overlay, {e:?}"))
+                .or_service_specific_exception(-1)?;
+        fs::write(&dt_output, fdt.as_slice()).or_service_specific_exception(-1)?;
+        Some(File::open(dt_output).or_service_specific_exception(-1)?)
+    } else {
+        None
+    };
+    Ok(device_tree_overlay)
+}
+
 fn write_zero_filler(zero_filler_path: &Path) -> Result<()> {
     let file = OpenOptions::new()
         .create_new(true)
@@ -857,6 +883,16 @@
     SUPPORTED_OS_NAMES.contains(os_name)
 }
 
+fn uses_gki_kernel(config: &VirtualMachineConfig) -> bool {
+    if !cfg!(vendor_modules) {
+        return false;
+    }
+    match config {
+        VirtualMachineConfig::RawConfig(_) => false,
+        VirtualMachineConfig::AppConfig(config) => config.osName.starts_with("microdroid_gki-"),
+    }
+}
+
 fn load_app_config(
     config: &VirtualMachineAppConfig,
     debug_config: &DebugConfig,
@@ -936,6 +972,7 @@
     vm_config.protectedVm = config.protectedVm;
     vm_config.cpuTopology = config.cpuTopology;
     vm_config.hugePages = config.hugePages || vm_payload_config.hugepages;
+    vm_config.boostUclamp = config.boostUclamp;
 
     // Microdroid takes additional init ramdisk & (optionally) storage image
     add_microdroid_system_images(config, instance_file, storage_image, os_name, &mut vm_config)?;
@@ -1203,6 +1240,10 @@
             .or_service_specific_exception(-1)?;
         Ok(vsock_stream_to_pfd(stream))
     }
+
+    fn setHostConsoleName(&self, ptsname: &str) -> binder::Result<()> {
+        self.instance.vm_context.global_context.setHostConsoleName(ptsname)
+    }
 }
 
 impl Drop for VirtualMachine {
diff --git a/virtualizationmanager/src/crosvm.rs b/virtualizationmanager/src/crosvm.rs
index d48ef7b..47ef91a 100644
--- a/virtualizationmanager/src/crosvm.rs
+++ b/virtualizationmanager/src/crosvm.rs
@@ -14,10 +14,11 @@
 
 //! Functions for running instances of `crosvm`.
 
-use crate::aidl::{remove_temporary_files, Cid, VirtualMachineCallbacks};
+use crate::aidl::{remove_temporary_files, Cid, GLOBAL_SERVICE, VirtualMachineCallbacks};
 use crate::atom::{get_num_cpus, write_vm_exited_stats_sync};
 use crate::debug_config::DebugConfig;
 use anyhow::{anyhow, bail, Context, Error, Result};
+use binder::ParcelFileDescriptor;
 use command_fds::CommandFdExt;
 use lazy_static::lazy_static;
 use libc::{sysconf, _SC_CLK_TCK};
@@ -34,7 +35,7 @@
 use std::io::{self, Read};
 use std::mem;
 use std::num::{NonZeroU16, NonZeroU32};
-use std::os::unix::io::{AsRawFd, RawFd};
+use std::os::unix::io::{AsRawFd, OwnedFd, RawFd};
 use std::os::unix::process::ExitStatusExt;
 use std::path::{Path, PathBuf};
 use std::process::{Command, ExitStatus};
@@ -46,6 +47,7 @@
     MemoryTrimLevel::MemoryTrimLevel,
     VirtualMachineAppConfig::DebugLevel::DebugLevel,
     DisplayConfig::DisplayConfig as DisplayConfigParcelable,
+    GpuConfig::GpuConfig as GpuConfigParcelable,
 };
 use android_system_virtualizationservice_internal::aidl::android::system::virtualizationservice_internal::IGlobalVmContext::IGlobalVmContext;
 use android_system_virtualizationservice_internal::aidl::android::system::virtualizationservice_internal::IBoundDevice::IBoundDevice;
@@ -82,6 +84,12 @@
 
 const SYSPROP_CUSTOM_PVMFW_PATH: &str = "hypervisor.pvmfw.path";
 
+/// Serial device for VM console input.
+/// Hypervisor (virtio-console)
+const CONSOLE_HVC0: &str = "hvc0";
+/// Serial (emulated uart)
+const CONSOLE_TTYS0: &str = "ttyS0";
+
 lazy_static! {
     /// If the VM doesn't move to the Started state within this amount time, a hang-up error is
     /// triggered.
@@ -122,6 +130,11 @@
     pub display_config: Option<DisplayConfig>,
     pub input_device_options: Vec<InputDeviceOption>,
     pub hugepages: bool,
+    pub tap: Option<File>,
+    pub virtio_snd_backend: Option<String>,
+    pub console_input_device: Option<String>,
+    pub boost_uclamp: bool,
+    pub gpu_config: Option<GpuConfig>,
 }
 
 #[derive(Debug)]
@@ -144,6 +157,37 @@
     }
 }
 
+#[derive(Debug)]
+pub struct GpuConfig {
+    pub backend: Option<String>,
+    pub context_types: Option<Vec<String>>,
+    pub pci_address: Option<String>,
+    pub renderer_features: Option<String>,
+    pub renderer_use_egl: Option<bool>,
+    pub renderer_use_gles: Option<bool>,
+    pub renderer_use_glx: Option<bool>,
+    pub renderer_use_surfaceless: Option<bool>,
+    pub renderer_use_vulkan: Option<bool>,
+}
+
+impl GpuConfig {
+    pub fn new(raw_config: &GpuConfigParcelable) -> Result<GpuConfig> {
+        Ok(GpuConfig {
+            backend: raw_config.backend.clone(),
+            context_types: raw_config.contextTypes.clone().map(|context_types| {
+                context_types.iter().filter_map(|context_type| context_type.clone()).collect()
+            }),
+            pci_address: raw_config.pciAddress.clone(),
+            renderer_features: raw_config.rendererFeatures.clone(),
+            renderer_use_egl: Some(raw_config.rendererUseEgl),
+            renderer_use_gles: Some(raw_config.rendererUseGles),
+            renderer_use_glx: Some(raw_config.rendererUseGlx),
+            renderer_use_surfaceless: Some(raw_config.rendererUseSurfaceless),
+            renderer_use_vulkan: Some(raw_config.rendererUseVulkan),
+        })
+    }
+}
+
 fn try_into_non_zero_u32(value: i32) -> Result<NonZeroU32> {
     let u32_value = value.try_into()?;
     NonZeroU32::new(u32_value).ok_or(anyhow!("value should be greater than 0"))
@@ -232,6 +276,8 @@
             let detect_hangup = config.detect_hangup;
             let (failure_pipe_read, failure_pipe_write) = create_pipe()?;
             let vfio_devices = config.vfio_devices.clone();
+            let tap =
+                if let Some(tap_file) = &config.tap { Some(tap_file.try_clone()?) } else { None };
 
             // If this fails and returns an error, `self` will be left in the `Failed` state.
             let child =
@@ -246,7 +292,7 @@
             let child_clone = child.clone();
             let instance_clone = instance.clone();
             let monitor_vm_exit_thread = Some(thread::spawn(move || {
-                instance_clone.monitor_vm_exit(child_clone, failure_pipe_read, vfio_devices);
+                instance_clone.monitor_vm_exit(child_clone, failure_pipe_read, vfio_devices, tap);
             }));
 
             if detect_hangup {
@@ -270,7 +316,7 @@
 #[derive(Debug)]
 pub struct VmContext {
     #[allow(dead_code)] // Keeps the global context alive
-    global_context: Strong<dyn IGlobalVmContext>,
+    pub(crate) global_context: Strong<dyn IGlobalVmContext>,
     #[allow(dead_code)] // Keeps the server alive
     vm_server: RpcServer,
 }
@@ -289,7 +335,7 @@
     pub vm_state: Mutex<VmState>,
     /// Global resources allocated for this VM.
     #[allow(dead_code)] // Keeps the context alive
-    vm_context: VmContext,
+    pub(crate) vm_context: VmContext,
     /// The CID assigned to the VM for vsock communication.
     pub cid: Cid,
     /// Path to crosvm control socket
@@ -388,6 +434,7 @@
         child: Arc<SharedChild>,
         mut failure_pipe_read: File,
         vfio_devices: Vec<VfioDevice>,
+        tap: Option<File>,
     ) {
         let result = child.wait();
         match &result {
@@ -447,6 +494,14 @@
             error!("Error removing temporary files from {:?}: {}", self.temporary_directory, e);
         });
 
+        if let Some(tap_file) = tap {
+            GLOBAL_SERVICE
+                .deleteTapInterface(&ParcelFileDescriptor::new(OwnedFd::from(tap_file)))
+                .unwrap_or_else(|e| {
+                    error!("Error deleting TAP interface: {e:?}");
+                });
+        }
+
         drop(vfio_devices); // Cleanup devices.
     }
 
@@ -833,6 +888,8 @@
         command.arg("--no-balloon");
     }
 
+    let mut memory_mib = config.memory_mib;
+
     if config.protected {
         match system_properties::read(SYSPROP_CUSTOM_PVMFW_PATH)? {
             Some(pvmfw_path) if !pvmfw_path.is_empty() => {
@@ -848,6 +905,12 @@
         let swiotlb_size_mib = 2 * virtio_pci_device_count as u32;
         command.arg("--swiotlb").arg(swiotlb_size_mib.to_string());
 
+        // b/346770542 for consistent "usable" memory across protected and non-protected VMs under
+        // pKVM.
+        if hypervisor_props::is_pkvm()? {
+            memory_mib = memory_mib.map(|m| m.saturating_add(swiotlb_size_mib));
+        }
+
         // Workaround to keep crash_dump from trying to read protected guest memory.
         // Context in b/238324526.
         command.arg("--unmap-guest-memory-on-fork");
@@ -869,7 +932,7 @@
         command.arg("--params").arg("console=hvc0");
     }
 
-    if let Some(memory_mib) = config.memory_mib {
+    if let Some(memory_mib) = memory_mib {
         command.arg("--mem").arg(memory_mib.to_string());
     }
 
@@ -917,19 +980,29 @@
     let log_arg = format_serial_out_arg(&mut preserved_fds, &config.log_fd);
     let failure_serial_path = add_preserved_fd(&mut preserved_fds, &failure_pipe_write);
     let ramdump_arg = format_serial_out_arg(&mut preserved_fds, &config.ramdump);
+    let console_input_device = config.console_input_device.as_deref().unwrap_or(CONSOLE_HVC0);
+    match console_input_device {
+        CONSOLE_HVC0 | CONSOLE_TTYS0 => {}
+        _ => bail!("Unsupported serial device {console_input_device}"),
+    };
 
     // Warning: Adding more serial devices requires you to shift the PCI device ID of the boot
     // disks in bootconfig.x86_64. This is because x86 crosvm puts serial devices and the block
     // devices in the same PCI bus and serial devices comes before the block devices. Arm crosvm
     // doesn't have the issue.
     // /dev/ttyS0
-    command.arg(format!("--serial={},hardware=serial,num=1", &console_out_arg));
+    command.arg(format!(
+        "--serial={}{},hardware=serial,num=1",
+        &console_out_arg,
+        if console_input_device == CONSOLE_TTYS0 { &console_in_arg } else { "" }
+    ));
     // /dev/ttyS1
     command.arg(format!("--serial=type=file,path={},hardware=serial,num=2", &failure_serial_path));
     // /dev/hvc0
     command.arg(format!(
         "--serial={}{},hardware=virtio-console,num=1",
-        &console_out_arg, &console_in_arg
+        &console_out_arg,
+        if console_input_device == CONSOLE_HVC0 { &console_in_arg } else { "" }
     ));
     // /dev/hvc1
     command.arg(format!("--serial={},hardware=virtio-console,num=2", &ramdump_arg));
@@ -969,21 +1042,56 @@
     }
 
     if cfg!(paravirtualized_devices) {
+        if let Some(gpu_config) = &config.gpu_config {
+            let mut gpu_args = Vec::new();
+            if let Some(backend) = &gpu_config.backend {
+                gpu_args.push(format!("backend={}", backend));
+            }
+            if let Some(context_types) = &gpu_config.context_types {
+                gpu_args.push(format!("context-types={}", context_types.join(":")));
+            }
+            if let Some(pci_address) = &gpu_config.pci_address {
+                gpu_args.push(format!("pci-address={}", pci_address));
+            }
+            if let Some(renderer_features) = &gpu_config.renderer_features {
+                gpu_args.push(format!("renderer-features={}", renderer_features));
+            }
+            if gpu_config.renderer_use_egl.unwrap_or(false) {
+                gpu_args.push("egl=true".to_string());
+            }
+            if gpu_config.renderer_use_gles.unwrap_or(false) {
+                gpu_args.push("gles=true".to_string());
+            }
+            if gpu_config.renderer_use_glx.unwrap_or(false) {
+                gpu_args.push("glx=true".to_string());
+            }
+            if gpu_config.renderer_use_surfaceless.unwrap_or(false) {
+                gpu_args.push("surfaceless=true".to_string());
+            }
+            if gpu_config.renderer_use_vulkan.unwrap_or(false) {
+                gpu_args.push("vulkan=true".to_string());
+            }
+            command.arg(format!("--gpu={}", gpu_args.join(",")));
+        }
         if let Some(display_config) = &config.display_config {
-            command.arg("--gpu")
-            // TODO(b/331708504): support backend config as well
-            .arg("backend=virglrenderer,context-types=virgl2,egl=true,surfaceless=true,glx=false,gles=true")
-            .arg(format!("--gpu-display=mode=windowed[{},{}],dpi=[{},{}],refresh-rate={}", display_config.width, display_config.height, display_config.horizontal_dpi, display_config.vertical_dpi, display_config.refresh_rate))
-            .arg(format!("--android-display-service={}", config.name));
+            command
+                .arg(format!(
+                    "--gpu-display=mode=windowed[{},{}],dpi=[{},{}],refresh-rate={}",
+                    display_config.width,
+                    display_config.height,
+                    display_config.horizontal_dpi,
+                    display_config.vertical_dpi,
+                    display_config.refresh_rate
+                ))
+                .arg(format!("--android-display-service={}", config.name));
         }
     }
 
-    if cfg!(paravirtualized_devices) {
-        // TODO(b/325929096): Need to set up network from the config
-        if rustutils::system_properties::read_bool("ro.crosvm.network.setup.done", false)
-            .unwrap_or(false)
-        {
-            command.arg("--net").arg("tap-name=crosvm_tap");
+    if cfg!(network) {
+        if let Some(tap) = &config.tap {
+            let tap_fd = tap.as_raw_fd();
+            preserved_fds.push(tap_fd);
+            command.arg("--net").arg(format!("tap-fd={}", tap_fd));
         }
     }
 
@@ -1015,11 +1123,21 @@
         command.arg("--hugepages");
     }
 
+    if config.boost_uclamp {
+        command.arg("--boost-uclamp");
+    }
+
     append_platform_devices(&mut command, &mut preserved_fds, &config)?;
 
     debug!("Preserving FDs {:?}", preserved_fds);
     command.preserved_fds(preserved_fds);
 
+    if cfg!(paravirtualized_devices) {
+        if let Some(virtio_snd_backend) = &config.virtio_snd_backend {
+            command.arg("--virtio-snd").arg(format!("backend={}", virtio_snd_backend));
+        }
+    }
+
     print_crosvm_args(&command);
 
     let result = SharedChild::spawn(&mut command)?;
diff --git a/virtualizationservice/Android.bp b/virtualizationservice/Android.bp
index 0c39501..f9034af 100644
--- a/virtualizationservice/Android.bp
+++ b/virtualizationservice/Android.bp
@@ -30,6 +30,7 @@
         "android.system.virtualizationservice-rust",
         "android.system.virtualizationservice_internal-rust",
         "android.system.virtualmachineservice-rust",
+        "android.system.vmtethering-rust",
         "android.os.permissions_aidl-rust",
         "libandroid_logger",
         "libanyhow",
diff --git a/virtualizationservice/aidl/Android.bp b/virtualizationservice/aidl/Android.bp
index fb89772..bca4512 100644
--- a/virtualizationservice/aidl/Android.bp
+++ b/virtualizationservice/aidl/Android.bp
@@ -86,6 +86,26 @@
 }
 
 aidl_interface {
+    name: "android.system.vmtethering",
+    srcs: ["android/system/vmtethering/**/*.aidl"],
+    unstable: true,
+    backend: {
+        java: {
+            sdk_version: "module_current",
+            apex_available: [
+                "com.android.virt",
+            ],
+        },
+        rust: {
+            enabled: true,
+            apex_available: [
+                "com.android.virt",
+            ],
+        },
+    },
+}
+
+aidl_interface {
     name: "android.system.virtualmachineservice",
     srcs: ["android/system/virtualmachineservice/**/*.aidl"],
     imports: [
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/GpuConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/GpuConfig.aidl
new file mode 100644
index 0000000..1cd4dc6
--- /dev/null
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/GpuConfig.aidl
@@ -0,0 +1,28 @@
+/*
+ * 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 android.system.virtualizationservice;
+
+parcelable GpuConfig {
+    @nullable String backend;
+    @nullable String[] contextTypes;
+    @nullable String pciAddress;
+    @nullable String rendererFeatures;
+    boolean rendererUseEgl = false;
+    boolean rendererUseGles = false;
+    boolean rendererUseGlx = false;
+    boolean rendererUseSurfaceless = false;
+    boolean rendererUseVulkan = false;
+}
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl
index d76b586..d4001c8 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl
@@ -47,4 +47,7 @@
 
     /** Open a vsock connection to the CID of the VM on the given port. */
     ParcelFileDescriptor connectVsock(int port);
+
+    /** Set the name of the peer end (ptsname) of the host console. */
+    void setHostConsoleName(in @utf8InCpp String pathname);
 }
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
index f8b5087..234d8d0 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
@@ -25,6 +25,7 @@
     const String FEATURE_DICE_CHANGES = "com.android.kvm.DICE_CHANGES";
     const String FEATURE_LLPVM_CHANGES = "com.android.kvm.LLPVM_CHANGES";
     const String FEATURE_MULTI_TENANT = "com.android.kvm.MULTI_TENANT";
+    const String FEATURE_NETWORK = "com.android.kvm.NETWORK";
     const String FEATURE_REMOTE_ATTESTATION = "com.android.kvm.REMOTE_ATTESTATION";
     const String FEATURE_VENDOR_MODULES = "com.android.kvm.VENDOR_MODULES";
 
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
index a3f4b0f..ee39d75 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
@@ -139,4 +139,7 @@
      *  https://docs.kernel.org/admin-guide/mm/transhuge.html
      */
     boolean hugePages;
+
+    /** Enable boost UClamp for less variance during testing/benchmarking */
+    boolean boostUclamp;
 }
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineDebugInfo.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineDebugInfo.aidl
index 870a342..9f033b1 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineDebugInfo.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineDebugInfo.aidl
@@ -33,4 +33,7 @@
      * the PID may have been reused for a different process, so this should not be trusted.
      */
     int requesterPid;
+
+    /** The peer end (ptsname) of the host console. */
+    @nullable @utf8InCpp String hostConsoleName;
 }
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
index a5a849a..69664b4 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
@@ -18,6 +18,7 @@
 import android.system.virtualizationservice.CpuTopology;
 import android.system.virtualizationservice.DiskImage;
 import android.system.virtualizationservice.DisplayConfig;
+import android.system.virtualizationservice.GpuConfig;
 import android.system.virtualizationservice.InputDevice;
 
 /** Raw configuration for running a VM. */
@@ -88,4 +89,12 @@
 
     /** Whether the VM should have network feature. */
     boolean networkSupported;
+
+    /** The serial device for VM console input. */
+    @nullable @utf8InCpp String consoleInputDevice;
+
+    /** Enable boost UClamp for less variance during testing/benchmarking */
+    boolean boostUclamp;
+
+    @nullable GpuConfig gpuConfig;
 }
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IGlobalVmContext.aidl b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IGlobalVmContext.aidl
index a4d5d19..ea52591 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IGlobalVmContext.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IGlobalVmContext.aidl
@@ -21,4 +21,7 @@
 
     /** Get the path to the temporary folder of the VM. */
     String getTemporaryDirectory();
+
+    /** Set the name of the peer end (ptsname) of the host console. */
+    void setHostConsoleName(@utf8InCpp String pathname);
 }
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
index 4e6879d..0da7755 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
@@ -128,4 +128,10 @@
      * @return file descriptor of the TAP network interface.
      */
     ParcelFileDescriptor createTapInterface(String ifaceNameSuffix);
+
+    /**
+     * Delete TAP network interface created for a VM.
+     * @param file descriptor of the TAP network interface.
+     */
+    void deleteTapInterface(in ParcelFileDescriptor tapFd);
 }
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVmnic.aidl b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVmnic.aidl
index 66739da..e3cc73a 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVmnic.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVmnic.aidl
@@ -22,4 +22,10 @@
      * @return file descriptor of the TAP network interface.
      */
     ParcelFileDescriptor createTapInterface(String ifaceNameSuffix);
+
+    /**
+     * Delete TAP network interface created for a VM.
+     * @param file descriptor of the TAP network interface.
+     */
+    void deleteTapInterface(in ParcelFileDescriptor tapFd);
 }
diff --git a/virtualizationservice/aidl/android/system/vmtethering/IVmTethering.aidl b/virtualizationservice/aidl/android/system/vmtethering/IVmTethering.aidl
new file mode 100644
index 0000000..0743ffa
--- /dev/null
+++ b/virtualizationservice/aidl/android/system/vmtethering/IVmTethering.aidl
@@ -0,0 +1,29 @@
+/*
+ * 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 android.system.vmtethering;
+
+interface IVmTethering {
+    /**
+     * Start VM tethering to provide external network to VM.
+     */
+    void enableVmTethering();
+
+    /**
+     * Terminate VM tethering that providing external network to VM.
+     */
+    void disableVmTethering();
+}
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index 5e71245..acdb53a 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -25,6 +25,7 @@
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice;
 use android_system_virtualizationservice_internal as android_vs_internal;
 use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice;
+use android_system_vmtethering::aidl::android::system::vmtethering;
 use android_vs_internal::aidl::android::system::virtualizationservice_internal;
 use anyhow::{anyhow, ensure, Context, Result};
 use avflog::LogResult;
@@ -33,7 +34,7 @@
     LazyServiceGuard, ParcelFileDescriptor, Status, Strong,
 };
 use lazy_static::lazy_static;
-use libc::VMADDR_CID_HOST;
+use libc::{VMADDR_CID_HOST, VMADDR_CID_HYPERVISOR, VMADDR_CID_LOCAL};
 use log::{error, info, warn};
 use nix::unistd::{chown, Uid};
 use openssl::x509::X509;
@@ -73,6 +74,7 @@
     IVmnic::{BpVmnic, IVmnic},
 };
 use virtualmachineservice::IVirtualMachineService::VM_TOMBSTONES_SERVICE_PORT;
+use vmtethering::IVmTethering::{BpVmTethering, IVmTethering};
 use vsock::{VsockListener, VsockStream};
 
 /// The unique ID of a VM used (together with a port number) for vsock communication.
@@ -163,6 +165,9 @@
     static ref NETWORK_SERVICE: Strong<dyn IVmnic> =
         wait_for_interface(<BpVmnic as IVmnic>::get_descriptor())
             .expect("Could not connect to Vmnic");
+    static ref TETHERING_SERVICE: Strong<dyn IVmTethering> =
+        wait_for_interface(<BpVmTethering as IVmTethering>::get_descriptor())
+            .expect("Could not connect to VmTethering");
 }
 
 fn is_valid_guest_cid(cid: Cid) -> bool {
@@ -282,11 +287,15 @@
             .held_contexts
             .iter()
             .filter_map(|(_, inst)| Weak::upgrade(inst))
-            .map(|vm| VirtualMachineDebugInfo {
-                cid: vm.cid as i32,
-                temporaryDirectory: vm.get_temp_dir().to_string_lossy().to_string(),
-                requesterUid: vm.requester_uid as i32,
-                requesterPid: vm.requester_debug_pid,
+            .map(|vm| {
+                let vm = vm.lock().unwrap();
+                VirtualMachineDebugInfo {
+                    cid: vm.cid as i32,
+                    temporaryDirectory: vm.get_temp_dir().to_string_lossy().to_string(),
+                    requesterUid: vm.requester_uid as i32,
+                    requesterPid: vm.requester_debug_pid,
+                    hostConsoleName: vm.host_console_name.clone(),
+                }
             })
             .collect();
         Ok(cids)
@@ -346,13 +355,14 @@
             ))
             .with_log();
         }
-        if !remotely_provisioned_component_service_exists()? {
+        if !is_remote_provisioning_hal_declared()? {
             return Err(Status::new_exception_str(
                 ExceptionCode::UNSUPPORTED_OPERATION,
                 Some("AVF remotely provisioned component service is not declared"),
             ))
             .with_log();
         }
+        remote_provisioning::check_remote_attestation_is_supported()?;
         info!("Received csr. Requestting attestation...");
         let (key_blob, certificate_chain) = if test_mode {
             check_use_custom_virtual_machine()?;
@@ -403,7 +413,8 @@
     }
 
     fn isRemoteAttestationSupported(&self) -> binder::Result<bool> {
-        remotely_provisioned_component_service_exists()
+        Ok(is_remote_provisioning_hal_declared()?
+            && remote_provisioning::is_remote_attestation_supported())
     }
 
     fn getAssignableDevices(&self) -> binder::Result<Vec<AssignableDevice>> {
@@ -452,7 +463,7 @@
             .context("Failed to allocate instance_id")
             .or_service_specific_exception(-1)?;
         let uid = get_calling_uid();
-        info!("Allocated a VM's instance_id: {:?}, for uid: {:?}", hex::encode(id), uid);
+        info!("Allocated a VM's instance_id: {:?}..., for uid: {:?}", &hex::encode(id)[..8], uid);
         let state = &mut *self.state.lock().unwrap();
         if let Some(sk_state) = &mut state.sk_state {
             let user_id = multiuser_get_user_id(uid);
@@ -469,8 +480,16 @@
     fn removeVmInstance(&self, instance_id: &[u8; 64]) -> binder::Result<()> {
         let state = &mut *self.state.lock().unwrap();
         if let Some(sk_state) = &mut state.sk_state {
-            info!("removeVmInstance(): delete secret");
-            sk_state.delete_ids(&[*instance_id]);
+            let uid = get_calling_uid();
+            info!(
+                "Removing a VM's instance_id: {:?}, for uid: {:?}",
+                hex::encode(instance_id),
+                uid
+            );
+
+            let user_id = multiuser_get_user_id(uid);
+            let app_id = multiuser_get_app_id(uid);
+            sk_state.delete_id(instance_id, user_id, app_id);
         } else {
             info!("ignoring removeVmInstance() as no ISecretkeeper");
         }
@@ -499,7 +518,8 @@
         Ok(())
     }
 
-    fn createTapInterface(&self, iface_name_suffix: &str) -> binder::Result<ParcelFileDescriptor> {
+    fn createTapInterface(&self, _iface_name_suffix: &str) -> binder::Result<ParcelFileDescriptor> {
+        check_internet_permission()?;
         check_use_custom_virtual_machine()?;
         if !cfg!(network) {
             return Err(Status::new_exception_str(
@@ -508,7 +528,33 @@
             ))
             .with_log();
         }
-        NETWORK_SERVICE.createTapInterface(iface_name_suffix)
+        // TODO(340377643): Use iface_name_suffix after introducing bridge interface, not fixed
+        // value.
+        let tap_fd = NETWORK_SERVICE.createTapInterface("fixed")?;
+
+        // TODO(340377643): Due to lack of implementation of creating bridge interface, tethering is
+        // enabled for TAP interface instead of bridge interface. After introducing creation of
+        // bridge interface in AVF, we should modify it.
+        TETHERING_SERVICE.enableVmTethering()?;
+
+        Ok(tap_fd)
+    }
+
+    fn deleteTapInterface(&self, tap_fd: &ParcelFileDescriptor) -> binder::Result<()> {
+        check_internet_permission()?;
+        check_use_custom_virtual_machine()?;
+        if !cfg!(network) {
+            return Err(Status::new_exception_str(
+                ExceptionCode::UNSUPPORTED_OPERATION,
+                Some("deleteTapInterface is not supported with the network feature disabled"),
+            ))
+            .with_log();
+        }
+
+        // TODO(340377643): Disabling tethering should be for bridge interface, not TAP interface.
+        TETHERING_SERVICE.disableVmTethering()?;
+
+        NETWORK_SERVICE.deleteTapInterface(tap_fd)
     }
 }
 
@@ -619,6 +665,8 @@
     requester_uid: uid_t,
     /// PID of the client who requested this VM instance.
     requester_debug_pid: pid_t,
+    /// Name of the host console.
+    host_console_name: Option<String>,
 }
 
 impl GlobalVmInstance {
@@ -633,7 +681,7 @@
 struct GlobalState {
     /// VM contexts currently allocated to running VMs. A CID is never recycled as long
     /// as there is a strong reference held by a GlobalVmContext.
-    held_contexts: HashMap<Cid, Weak<GlobalVmInstance>>,
+    held_contexts: HashMap<Cid, Weak<Mutex<GlobalVmInstance>>>,
 
     /// Cached read-only FD of VM DTBO file. Also serves as a lock for creating the file.
     dtbo_file: Mutex<Option<File>>,
@@ -713,8 +761,13 @@
         self.held_contexts.retain(|_, instance| instance.strong_count() > 0);
 
         let cid = self.get_next_available_cid()?;
-        let instance = Arc::new(GlobalVmInstance { cid, requester_uid, requester_debug_pid });
-        create_temporary_directory(&instance.get_temp_dir(), Some(requester_uid))?;
+        let instance = Arc::new(Mutex::new(GlobalVmInstance {
+            cid,
+            requester_uid,
+            requester_debug_pid,
+            ..Default::default()
+        }));
+        create_temporary_directory(&instance.lock().unwrap().get_temp_dir(), Some(requester_uid))?;
 
         self.held_contexts.insert(cid, Arc::downgrade(&instance));
         let binder = GlobalVmContext { instance, ..Default::default() };
@@ -794,7 +847,7 @@
 #[derive(Debug, Default)]
 struct GlobalVmContext {
     /// Strong reference to the context's instance data structure.
-    instance: Arc<GlobalVmInstance>,
+    instance: Arc<Mutex<GlobalVmInstance>>,
     /// Keeps our service process running as long as this VM context exists.
     #[allow(dead_code)]
     lazy_service_guard: LazyServiceGuard,
@@ -804,11 +857,16 @@
 
 impl IGlobalVmContext for GlobalVmContext {
     fn getCid(&self) -> binder::Result<i32> {
-        Ok(self.instance.cid as i32)
+        Ok(self.instance.lock().unwrap().cid as i32)
     }
 
     fn getTemporaryDirectory(&self) -> binder::Result<String> {
-        Ok(self.instance.get_temp_dir().to_string_lossy().to_string())
+        Ok(self.instance.lock().unwrap().get_temp_dir().to_string_lossy().to_string())
+    }
+
+    fn setHostConsoleName(&self, pathname: &str) -> binder::Result<()> {
+        self.instance.lock().unwrap().host_console_name = Some(pathname.to_string());
+        Ok(())
     }
 }
 
@@ -820,11 +878,21 @@
     for incoming_stream in listener.incoming() {
         let mut incoming_stream = match incoming_stream {
             Err(e) => {
-                warn!("invalid incoming connection: {:?}", e);
+                warn!("invalid incoming connection: {e:?}");
                 continue;
             }
             Ok(s) => s,
         };
+        if let Ok(addr) = incoming_stream.peer_addr() {
+            let cid = addr.cid();
+            match cid {
+                VMADDR_CID_LOCAL | VMADDR_CID_HOST | VMADDR_CID_HYPERVISOR => {
+                    warn!("Rejecting non-guest tombstone vsock connection from cid={cid}");
+                    continue;
+                }
+                _ => info!("Vsock Stream connected to cid={cid} for tombstones"),
+            }
+        }
         std::thread::spawn(move || {
             if let Err(e) = handle_tombstone(&mut incoming_stream) {
                 error!("Failed to write tombstone- {:?}", e);
@@ -835,9 +903,6 @@
 }
 
 fn handle_tombstone(stream: &mut VsockStream) -> Result<()> {
-    if let Ok(addr) = stream.peer_addr() {
-        info!("Vsock Stream connected to cid={} for tombstones", addr.cid());
-    }
     let tb_connection =
         TombstonedConnection::connect(std::process::id() as i32, DebuggerdDumpType::Tombstone)
             .context("Failed to connect to tombstoned")?;
@@ -862,7 +927,9 @@
     Ok(())
 }
 
-fn remotely_provisioned_component_service_exists() -> binder::Result<bool> {
+/// Returns true if the AVF remotely provisioned component service is declared in the
+/// VINTF manifest.
+pub(crate) fn is_remote_provisioning_hal_declared() -> binder::Result<bool> {
     Ok(binder::is_declared(REMOTELY_PROVISIONED_COMPONENT_SERVICE_NAME)?)
 }
 
@@ -899,6 +966,12 @@
     check_permission("android.permission.USE_CUSTOM_VIRTUAL_MACHINE")
 }
 
+/// Check whether the caller of the current Binder method is allowed to create socket and
+/// establish connection between the VM and the Internet.
+fn check_internet_permission() -> binder::Result<()> {
+    check_permission("android.permission.INTERNET")
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/virtualizationservice/src/main.rs b/virtualizationservice/src/main.rs
index 8acfdd3..55245f6 100644
--- a/virtualizationservice/src/main.rs
+++ b/virtualizationservice/src/main.rs
@@ -20,7 +20,10 @@
 mod remote_provisioning;
 mod rkpvm;
 
-use crate::aidl::{remove_temporary_dir, VirtualizationServiceInternal, TEMPORARY_DIRECTORY};
+use crate::aidl::{
+    is_remote_provisioning_hal_declared, remove_temporary_dir, VirtualizationServiceInternal,
+    TEMPORARY_DIRECTORY,
+};
 use android_logger::{Config, FilterBuilder};
 use android_system_virtualizationmaintenance::aidl::android::system::virtualizationmaintenance;
 use android_system_virtualizationservice_internal::aidl::android::system::virtualizationservice_internal;
@@ -81,7 +84,7 @@
         BnVirtualizationServiceInternal::new_binder(service.clone(), BinderFeatures::default());
     register(INTERNAL_SERVICE_NAME, internal_service)?;
 
-    if cfg!(remote_attestation) {
+    if is_remote_provisioning_hal_declared().unwrap_or(false) {
         // The IRemotelyProvisionedComponent service is only supposed to be triggered by rkpd for
         // RKP VM attestation.
         let remote_provisioning_service = remote_provisioning::new_binder();
diff --git a/virtualizationservice/src/maintenance.rs b/virtualizationservice/src/maintenance.rs
index 4732e1f..8e04075 100644
--- a/virtualizationservice/src/maintenance.rs
+++ b/virtualizationservice/src/maintenance.rs
@@ -90,14 +90,15 @@
         self.get_inner()?.delete_ids_for_app(user_id, app_id)
     }
 
-    /// Delete the provided VM IDs from both Secretkeeper and the database.
-    pub fn delete_ids(&mut self, vm_ids: &[VmId]) {
+    /// Delete the provided VM ID associated with `(user_id, app_id)` from both Secretkeeper and
+    /// the database.
+    pub fn delete_id(&mut self, vm_id: &VmId, user_id: u32, app_id: u32) {
         let Ok(inner) = self.get_inner() else {
             warn!("No Secretkeeper available, not deleting secrets");
             return;
         };
 
-        inner.delete_ids(vm_ids)
+        inner.delete_id_for_app(vm_id, user_id, app_id)
     }
 
     /// Perform reconciliation to allow for possibly missed notifications of user or app removal.
@@ -157,6 +158,16 @@
         self.vm_id_db.add_vm_id(vm_id, user_id, app_id)
     }
 
+    fn delete_id_for_app(&mut self, vm_id: &VmId, user_id: u32, app_id: u32) {
+        if !self.vm_id_db.is_vm_id_for_app(vm_id, user_id, app_id).unwrap_or(false) {
+            info!(
+                "delete_id_for_app - VM id not associated with user_id={user_id}, app_id={app_id}"
+            );
+            return;
+        }
+        self.delete_ids(&[*vm_id])
+    }
+
     fn delete_ids_for_user(&mut self, user_id: i32) -> Result<()> {
         let vm_ids = self.vm_id_db.vm_ids_for_user(user_id)?;
         info!(
@@ -371,8 +382,8 @@
     #[test]
     fn test_sk_state_batching() {
         let history = Arc::new(Mutex::new(Vec::new()));
-        let mut sk_state = new_test_state(history.clone(), 2);
-        sk_state.delete_ids(&[VM_ID1, VM_ID2, VM_ID3, VM_ID4, VM_ID5]);
+        let sk_state = new_test_state(history.clone(), 2);
+        sk_state.inner.unwrap().delete_ids(&[VM_ID1, VM_ID2, VM_ID3, VM_ID4, VM_ID5]);
         let got = (*history.lock().unwrap()).clone();
         assert_eq!(
             got,
@@ -387,8 +398,8 @@
     #[test]
     fn test_sk_state_no_batching() {
         let history = Arc::new(Mutex::new(Vec::new()));
-        let mut sk_state = new_test_state(history.clone(), 6);
-        sk_state.delete_ids(&[VM_ID1, VM_ID2, VM_ID3, VM_ID4, VM_ID5]);
+        let sk_state = new_test_state(history.clone(), 6);
+        sk_state.inner.unwrap().delete_ids(&[VM_ID1, VM_ID2, VM_ID3, VM_ID4, VM_ID5]);
         let got = (*history.lock().unwrap()).clone();
         assert_eq!(got, vec![SkOp::DeleteIds(vec![VM_ID1, VM_ID2, VM_ID3, VM_ID4, VM_ID5])]);
     }
@@ -402,7 +413,7 @@
         get_db(&mut sk_state).add_vm_id(&VM_ID2, USER1, APP_A).unwrap();
         get_db(&mut sk_state).add_vm_id(&VM_ID3, USER2, APP_B).unwrap();
         get_db(&mut sk_state).add_vm_id(&VM_ID4, USER3, APP_A).unwrap();
-        get_db(&mut sk_state).add_vm_id(&VM_ID5, USER3, APP_C).unwrap(); // Overwrites APP_A
+        get_db(&mut sk_state).add_vm_id(&VM_ID5, USER3, APP_C).unwrap();
         assert_eq!((*history.lock().unwrap()).clone(), vec![]);
 
         sk_state.delete_ids_for_app(USER2, APP_B).unwrap();
@@ -425,6 +436,36 @@
     }
 
     #[test]
+    fn test_sk_state_delete_id() {
+        let history = Arc::new(Mutex::new(Vec::new()));
+        let mut sk_state = new_test_state(history.clone(), 2);
+
+        get_db(&mut sk_state).add_vm_id(&VM_ID1, USER1, APP_A).unwrap();
+        get_db(&mut sk_state).add_vm_id(&VM_ID2, USER1, APP_A).unwrap();
+        get_db(&mut sk_state).add_vm_id(&VM_ID3, USER2, APP_B).unwrap();
+        assert_eq!((*history.lock().unwrap()).clone(), vec![]);
+
+        // A VM ID that doesn't exist anywhere - no delete
+        sk_state.delete_id(&VM_ID4, USER1 as u32, APP_A as u32);
+        assert_eq!((*history.lock().unwrap()).clone(), vec![]);
+
+        // Wrong app ID - no delete
+        sk_state.delete_id(&VM_ID1, USER1 as u32, APP_B as u32);
+        assert_eq!((*history.lock().unwrap()).clone(), vec![]);
+
+        // Wrong user ID - no delete
+        sk_state.delete_id(&VM_ID1, USER2 as u32, APP_A as u32);
+        assert_eq!((*history.lock().unwrap()).clone(), vec![]);
+
+        // This porridge is just right.
+        sk_state.delete_id(&VM_ID1, USER1 as u32, APP_A as u32);
+        assert_eq!((*history.lock().unwrap()).clone(), vec![SkOp::DeleteIds(vec![VM_ID1])]);
+
+        assert_eq!(vec![VM_ID2], get_db(&mut sk_state).vm_ids_for_user(USER1).unwrap());
+        assert_eq!(vec![VM_ID3], get_db(&mut sk_state).vm_ids_for_user(USER2).unwrap());
+    }
+
+    #[test]
     fn test_sk_state_reconcile() {
         let history = Arc::new(Mutex::new(Vec::new()));
         let mut sk_state = new_test_state(history.clone(), 20);
diff --git a/virtualizationservice/src/maintenance/vmdb.rs b/virtualizationservice/src/maintenance/vmdb.rs
index 273f340..3519015 100644
--- a/virtualizationservice/src/maintenance/vmdb.rs
+++ b/virtualizationservice/src/maintenance/vmdb.rs
@@ -272,6 +272,21 @@
         Ok(vm_ids)
     }
 
+    /// Determine whether the specified VM ID is associated with `(user_id, app_id)`. Returns false
+    /// if there is no such VM ID, or it exists but is not associated.
+    pub fn is_vm_id_for_app(&mut self, vm_id: &VmId, user_id: u32, app_id: u32) -> Result<bool> {
+        let mut stmt = self
+            .conn
+            .prepare(
+                "SELECT COUNT(*) FROM main.vmids \
+                        WHERE vm_id = ? AND user_id = ? AND app_id = ?;",
+            )
+            .context("failed to prepare SELECT stmt")?;
+        stmt.query_row(params![vm_id, user_id, app_id], |row| row.get(0))
+            .context("query failed")
+            .map(|n: usize| n != 0)
+    }
+
     /// Determine the number of VM IDs associated with `(user_id, app_id)`.
     pub fn count_vm_ids_for_app(&mut self, user_id: i32, app_id: i32) -> Result<usize> {
         let mut stmt = self
@@ -350,6 +365,7 @@
     const VM_ID3: VmId = [3u8; 64];
     const VM_ID4: VmId = [4u8; 64];
     const VM_ID5: VmId = [5u8; 64];
+    const VM_ID_UNKNOWN: VmId = [6u8; 64];
     const USER1: i32 = 1;
     const USER2: i32 = 2;
     const USER3: i32 = 3;
@@ -506,6 +522,13 @@
         assert_eq!(empty, db.vm_ids_for_app(USER1, APP_UNKNOWN).unwrap());
         assert_eq!(0, db.count_vm_ids_for_app(USER1, APP_UNKNOWN).unwrap());
 
+        assert!(db.is_vm_id_for_app(&VM_ID1, USER1 as u32, APP_A as u32).unwrap());
+        assert!(!db.is_vm_id_for_app(&VM_ID1, USER2 as u32, APP_A as u32).unwrap());
+        assert!(!db.is_vm_id_for_app(&VM_ID1, USER1 as u32, APP_B as u32).unwrap());
+        assert!(!db.is_vm_id_for_app(&VM_ID_UNKNOWN, USER1 as u32, APP_A as u32).unwrap());
+        assert!(!db.is_vm_id_for_app(&VM_ID5, USER3 as u32, APP_A as u32).unwrap());
+        assert!(db.is_vm_id_for_app(&VM_ID5, USER3 as u32, APP_C as u32).unwrap());
+
         db.delete_vm_ids(&[VM_ID2, VM_ID3]).unwrap();
 
         assert_eq!(vec![VM_ID1], db.vm_ids_for_user(USER1).unwrap());
diff --git a/virtualizationservice/src/remote_provisioning.rs b/virtualizationservice/src/remote_provisioning.rs
index c2c04df..490ff01 100644
--- a/virtualizationservice/src/remote_provisioning.rs
+++ b/virtualizationservice/src/remote_provisioning.rs
@@ -32,6 +32,7 @@
     Strong,
 };
 use hypervisor_props::is_protected_vm_supported;
+use rustutils::system_properties;
 use service_vm_comm::{RequestProcessingError, Response};
 
 /// Constructs a binder object that implements `IRemotelyProvisionedComponent`.
@@ -49,7 +50,7 @@
 #[allow(non_snake_case)]
 impl IRemotelyProvisionedComponent for AvfRemotelyProvisionedComponent {
     fn getHardwareInfo(&self) -> BinderResult<RpcHardwareInfo> {
-        check_protected_vm_is_supported()?;
+        check_remote_attestation_is_supported()?;
 
         Ok(RpcHardwareInfo {
             versionNumber: 3,
@@ -65,7 +66,7 @@
         testMode: bool,
         macedPublicKey: &mut MacedPublicKey,
     ) -> BinderResult<Vec<u8>> {
-        check_protected_vm_is_supported()?;
+        check_remote_attestation_is_supported()?;
 
         if testMode {
             return Err(Status::new_service_specific_error_str(
@@ -109,7 +110,7 @@
         keysToSign: &[MacedPublicKey],
         challenge: &[u8],
     ) -> BinderResult<Vec<u8>> {
-        check_protected_vm_is_supported()?;
+        check_remote_attestation_is_supported()?;
 
         const MAX_CHALLENGE_SIZE: usize = 64;
         if challenge.len() > MAX_CHALLENGE_SIZE {
@@ -133,16 +134,27 @@
     }
 }
 
-fn check_protected_vm_is_supported() -> BinderResult<()> {
-    if is_protected_vm_supported().unwrap_or(false) {
-        Ok(())
-    } else {
-        Err(Status::new_exception_str(
+pub(crate) fn check_remote_attestation_is_supported() -> BinderResult<()> {
+    if !is_protected_vm_supported().unwrap_or(false) {
+        return Err(Status::new_exception_str(
             ExceptionCode::UNSUPPORTED_OPERATION,
             Some("Protected VM support is missing for this operation"),
         ))
-        .with_log()
+        .with_log();
     }
+    if !is_remote_attestation_supported() {
+        return Err(Status::new_exception_str(
+            ExceptionCode::UNSUPPORTED_OPERATION,
+            Some("Remote attestation is disabled"),
+        ))
+        .with_log();
+    }
+    Ok(())
+}
+
+pub(crate) fn is_remote_attestation_supported() -> bool {
+    // Remote attestation is enabled by default.
+    system_properties::read_bool("avf.remote_attestation.enabled", true).unwrap_or(true)
 }
 
 pub(crate) fn to_service_specific_error(response: Response) -> Status {
diff --git a/virtualizationservice/vmnic/Android.bp b/virtualizationservice/vmnic/Android.bp
index 784c648..247be85 100644
--- a/virtualizationservice/vmnic/Android.bp
+++ b/virtualizationservice/vmnic/Android.bp
@@ -14,7 +14,9 @@
         "libandroid_logger",
         "libanyhow",
         "libbinder_rs",
+        "liblibc",
         "liblog_rust",
+        "libnix",
     ],
     apex_available: ["com.android.virt"],
 }
diff --git a/virtualizationservice/vmnic/src/aidl.rs b/virtualizationservice/vmnic/src/aidl.rs
index 6443258..03819b8 100644
--- a/virtualizationservice/vmnic/src/aidl.rs
+++ b/virtualizationservice/vmnic/src/aidl.rs
@@ -14,10 +14,64 @@
 
 //! Implementation of the AIDL interface of Vmnic.
 
-use anyhow::anyhow;
+use anyhow::{anyhow, Context, Result};
 use android_system_virtualizationservice_internal::aidl::android::system::virtualizationservice_internal::IVmnic::IVmnic;
-use binder::{self, ExceptionCode, Interface, IntoBinderResult, ParcelFileDescriptor};
+use binder::{self, Interface, IntoBinderResult, ParcelFileDescriptor};
+use libc::{c_char, c_int, c_short, ifreq, IFF_NO_PI, IFF_TAP, IFF_UP, IFF_VNET_HDR, IFNAMSIZ};
 use log::info;
+use nix::ioctl_write_ptr_bad;
+use nix::sys::ioctl::ioctl_num_type;
+use nix::sys::socket::{socket, AddressFamily, SockFlag, SockType};
+use std::ffi::{CStr, CString};
+use std::fs::OpenOptions;
+use std::os::fd::{AsRawFd, RawFd};
+use std::slice::from_raw_parts;
+
+const TUNGETIFF: ioctl_num_type = 0x800454d2u32 as ioctl_num_type;
+const TUNSETIFF: ioctl_num_type = 0x400454ca;
+const SIOCSIFFLAGS: ioctl_num_type = 0x00008914;
+
+ioctl_write_ptr_bad!(ioctl_tungetiff, TUNGETIFF, ifreq);
+ioctl_write_ptr_bad!(ioctl_tunsetiff, TUNSETIFF, ifreq);
+ioctl_write_ptr_bad!(ioctl_siocsifflags, SIOCSIFFLAGS, ifreq);
+
+fn validate_ifname(ifname: &[c_char]) -> Result<()> {
+    if ifname.len() >= IFNAMSIZ {
+        return Err(anyhow!(format!("Interface name is too long")));
+    }
+    Ok(())
+}
+
+fn create_tap_interface(fd: RawFd, sockfd: c_int, ifname: &[c_char]) -> Result<()> {
+    // SAFETY: All-zero is a valid value for the ifreq type.
+    let mut ifr: ifreq = unsafe { std::mem::zeroed() };
+    ifr.ifr_ifru.ifru_flags = (IFF_TAP | IFF_NO_PI | IFF_VNET_HDR) as c_short;
+    ifr.ifr_name[..ifname.len()].copy_from_slice(ifname);
+    // SAFETY: It modifies the state in the kernel, not the state of this process in any way.
+    unsafe { ioctl_tunsetiff(fd, &ifr) }.context("Failed to ioctl TUNSETIFF")?;
+    // SAFETY: ifr_ifru holds ifru_flags in its union field.
+    unsafe { ifr.ifr_ifru.ifru_flags |= IFF_UP as c_short };
+    // SAFETY: It modifies the state in the kernel, not the state of this process in any way.
+    unsafe { ioctl_siocsifflags(sockfd, &ifr) }.context("Failed to ioctl SIOCSIFFLAGS")?;
+    Ok(())
+}
+
+fn get_tap_ifreq(fd: RawFd) -> Result<ifreq> {
+    // SAFETY: All-zero is a valid value for the ifreq type.
+    let ifr: ifreq = unsafe { std::mem::zeroed() };
+    // SAFETY: Returned `ifr` of given file descriptor is set from TUNSETIFF ioctl while executing
+    // create_tap_interface(fd, sockfd, ifname). So the variable `ifr` should be safe.
+    unsafe { ioctl_tungetiff(fd, &ifr) }.context("Failed to ioctl TUNGETIFF")?;
+    Ok(ifr)
+}
+
+fn delete_tap_interface(sockfd: c_int, ifr: &mut ifreq) -> Result<()> {
+    // SAFETY: After calling TUNGETIFF, ifr_ifru holds ifru_flags in its union field.
+    unsafe { ifr.ifr_ifru.ifru_flags &= !IFF_UP as c_short };
+    // SAFETY: It modifies the state in the kernel, not the state of this process in any way.
+    unsafe { ioctl_siocsifflags(sockfd, ifr) }.context("Failed to ioctl SIOCSIFFLAGS")?;
+    Ok(())
+}
 
 #[derive(Debug, Default)]
 pub struct Vmnic {}
@@ -32,10 +86,52 @@
 
 impl IVmnic for Vmnic {
     fn createTapInterface(&self, iface_name_suffix: &str) -> binder::Result<ParcelFileDescriptor> {
-        let ifname = format!("avf_tap_{iface_name_suffix}");
-        info!("Creating TAP interface {}", ifname);
+        let ifname = CString::new(format!("avf_tap_{iface_name_suffix}"))
+            .context(format!(
+                "Failed to construct TAP interface name as CString: avf_tap_{iface_name_suffix}"
+            ))
+            .or_service_specific_exception(-1)?;
+        let ifname_bytes = ifname.as_bytes_with_nul();
+        // SAFETY: Converting from &[u8] into &[c_char].
+        let ifname_bytes =
+            unsafe { from_raw_parts(ifname_bytes.as_ptr().cast::<c_char>(), ifname_bytes.len()) };
+        validate_ifname(ifname_bytes)
+            .context(format!("Invalid interface name: {ifname:#?}"))
+            .or_service_specific_exception(-1)?;
 
-        Err(anyhow!("Creating TAP network interface is not supported yet"))
-            .or_binder_exception(ExceptionCode::UNSUPPORTED_OPERATION)
+        let tunfd = OpenOptions::new()
+            .read(true)
+            .write(true)
+            .open("/dev/tun")
+            .context("Failed to open /dev/tun")
+            .or_service_specific_exception(-1)?;
+        let sock = socket(AddressFamily::Inet, SockType::Datagram, SockFlag::empty(), None)
+            .context("Failed to create socket")
+            .or_service_specific_exception(-1)?;
+        create_tap_interface(tunfd.as_raw_fd(), sock.as_raw_fd(), ifname_bytes)
+            .context(format!("Failed to create TAP interface: {ifname:#?}"))
+            .or_service_specific_exception(-1)?;
+
+        info!("Created TAP network interface: {ifname:#?}");
+        Ok(ParcelFileDescriptor::new(tunfd))
+    }
+
+    fn deleteTapInterface(&self, tapfd: &ParcelFileDescriptor) -> binder::Result<()> {
+        let mut tap_ifreq = get_tap_ifreq(tapfd.as_raw_fd())
+            .context("Failed to get ifreq of TAP interface")
+            .or_service_specific_exception(-1)?;
+        // SAFETY: tap_ifreq.ifr_name is null-terminated within IFNAMSIZ, validated when creating
+        // TAP interface.
+        let ifname = unsafe { CStr::from_ptr(tap_ifreq.ifr_name.as_ptr()) };
+
+        let sock = socket(AddressFamily::Inet, SockType::Datagram, SockFlag::empty(), None)
+            .context("Failed to create socket")
+            .or_service_specific_exception(-1)?;
+        delete_tap_interface(sock.as_raw_fd(), &mut tap_ifreq)
+            .context(format!("Failed to create TAP interface: {ifname:#?}"))
+            .or_service_specific_exception(-1)?;
+
+        info!("Deleted TAP network interface: {ifname:#?}");
+        Ok(())
     }
 }
diff --git a/vm/src/main.rs b/vm/src/main.rs
index 390a60d..3c0887c 100644
--- a/vm/src/main.rs
+++ b/vm/src/main.rs
@@ -24,15 +24,18 @@
 };
 #[cfg(not(llpvm_changes))]
 use anyhow::anyhow;
-use anyhow::{Context, Error};
+use anyhow::{bail, Context, Error};
 use binder::{ProcessState, Strong};
 use clap::{Args, Parser};
 use create_idsig::command_create_idsig;
 use create_partition::command_create_partition;
 use run::{command_run, command_run_app, command_run_microdroid};
 use serde::Serialize;
+use std::io::{self, IsTerminal};
 use std::num::NonZeroU16;
+use std::os::unix::process::CommandExt;
 use std::path::{Path, PathBuf};
+use std::process::Command;
 
 #[derive(Args, Default)]
 /// Collection of flags that are at VM level and therefore applicable to all subcommands
@@ -65,6 +68,10 @@
     #[cfg(network)]
     #[arg(short, long)]
     network_supported: bool,
+
+    /// Boost uclamp to stablise results for benchmarks.
+    #[arg(short, long)]
+    boost_uclamp: bool,
 }
 
 impl CommonConfig {
@@ -320,6 +327,11 @@
         /// Path to idsig of the APK
         path: PathBuf,
     },
+    /// Connect to the serial console of a VM
+    Console {
+        /// CID of the VM
+        cid: Option<i32>,
+    },
 }
 
 fn parse_debug_level(s: &str) -> Result<DebugLevel, String> {
@@ -382,6 +394,7 @@
         Opt::CreateIdsig { apk, path } => {
             command_create_idsig(get_service()?.as_ref(), &apk, &path)
         }
+        Opt::Console { cid } => command_console(cid),
     }
 }
 
@@ -446,6 +459,21 @@
     Ok(())
 }
 
+fn command_console(cid: Option<i32>) -> Result<(), Error> {
+    if !io::stdin().is_terminal() {
+        bail!("Stdin must be a terminal (tty). Use 'adb shell -t' to force allocate tty.");
+    }
+    let mut vms = get_service()?.debugListVms().context("Failed to get list of VMs")?;
+    if let Some(cid) = cid {
+        vms.retain(|vm_info| vm_info.cid == cid);
+    }
+    let host_console_name = vms
+        .into_iter()
+        .find_map(|vm_info| vm_info.hostConsoleName)
+        .context("Failed to get VM with console")?;
+    Err(Command::new("microcom").arg(host_console_name).exec().into())
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/vm/src/run.rs b/vm/src/run.rs
index 0c9fbb6..cb15802 100644
--- a/vm/src/run.rs
+++ b/vm/src/run.rs
@@ -179,6 +179,7 @@
         customConfig: Some(custom_config),
         osName: os_name,
         hugePages: config.common.hugepages,
+        boostUclamp: config.common.boost_uclamp,
     });
     run(
         service.as_ref(),
@@ -260,6 +261,7 @@
     }
     vm_config.cpuTopology = config.common.cpu_topology;
     vm_config.hugePages = config.common.hugepages;
+    vm_config.boostUclamp = config.common.boost_uclamp;
     run(
         get_service()?.as_ref(),
         &VirtualMachineConfig::RawConfig(vm_config),
diff --git a/vm_payload/Android.bp b/vm_payload/Android.bp
index 229f533..cf2a002 100644
--- a/vm_payload/Android.bp
+++ b/vm_payload/Android.bp
@@ -39,8 +39,8 @@
     visibility: [":__subpackages__"],
 }
 
-// Rust wrappers round the C API for Rust clients.
-// (Yes, this involves going Rust -> C -> Rust.)
+// Access to the C API for Rust code.
+// This shouldn't be used directly - prefer libvm_payload_rs (below)
 rust_bindgen {
     name: "libvm_payload_bindgen",
     wrapper_src: "include-restricted/vm_payload_restricted.h",
@@ -51,15 +51,31 @@
     bindgen_flags: [
         "--default-enum-style rust",
     ],
-    visibility: [
-        "//packages/modules/Virtualization/compos",
-        "//packages/modules/Virtualization/service_vm:__subpackages__",
-    ],
     shared_libs: [
         "libvm_payload#current",
     ],
 }
 
+// Wrapper library for the raw C API for use by Rust clients.
+// (Yes, this involves going Rust -> C -> Rust.)
+// This is not a stable API - we may change it in subsequent versions.
+// But it is made available as an rlib so it is linked into any
+// code using it, leaving only dependencies on stable APIs.
+// So code built with it should run unchanged on future versions.
+rust_library_rlib {
+    name: "libvm_payload_rs",
+    crate_name: "vm_payload",
+    defaults: ["avf_build_flags_rust"],
+    srcs: ["wrapper/lib.rs"],
+    rustlibs: [
+        "libbinder_rs",
+        "libstatic_assertions",
+        "libvm_payload_bindgen",
+    ],
+    apex_available: ["com.android.compos"],
+    visibility: ["//visibility:public"],
+}
+
 // Shared library for clients to link against.
 cc_library_shared {
     name: "libvm_payload",
diff --git a/vm_payload/README.md b/vm_payload/README.md
index 4b1e6f3..66fd532 100644
--- a/vm_payload/README.md
+++ b/vm_payload/README.md
@@ -70,3 +70,16 @@
 See [AIDL
 backends](https://source.android.com/docs/core/architecture/aidl/aidl-backends)
 for information on using AIDL with the NDK Binder from C++.
+
+## Rust
+
+A Rust wrapper library for the VM Payload API is available (as an rlib) for VM
+payloads written in Rust.
+
+This wrapper is not guaranteed to be stable; we may change it in future
+versions. But payload code built using it will depend only on the C VM Payload
+API and the NDK APIs that are available to the payload, so should run unchanged
+on future versions.
+
+See [wrapper/lib.rs](wrapper/lib.rs) and `libvm_payload_rs` in
+[Android.bp](Android.bp).
diff --git a/vm_payload/wrapper/attestation.rs b/vm_payload/wrapper/attestation.rs
new file mode 100644
index 0000000..e0055d5
--- /dev/null
+++ b/vm_payload/wrapper/attestation.rs
@@ -0,0 +1,288 @@
+/*
+ * 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.
+ */
+
+use std::error::Error;
+use std::ffi::{c_void, CStr};
+use std::fmt::{self, Display};
+use std::iter::FusedIterator;
+use std::ptr::{self, NonNull};
+
+use vm_payload_bindgen::{
+    AVmAttestationResult, AVmAttestationResult_free, AVmAttestationResult_getCertificateAt,
+    AVmAttestationResult_getCertificateCount, AVmAttestationResult_getPrivateKey,
+    AVmAttestationResult_sign, AVmAttestationStatus, AVmAttestationStatus_toString,
+    AVmPayload_requestAttestation, AVmPayload_requestAttestationForTesting,
+};
+
+/// Holds the result of a successful Virtual Machine attestation request.
+/// See [`request_attestation`].
+#[derive(Debug)]
+pub struct AttestationResult {
+    result: NonNull<AVmAttestationResult>,
+}
+
+/// Error type that can be returned from an unsuccessful Virtual Machine attestation request.
+/// See [`request_attestation`].
+#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
+pub enum AttestationError {
+    /// The challenge size was not between 0 and 64 bytes (inclusive).
+    InvalidChallenge,
+    /// The attempt to attest the VM failed. A subsequent request may succeed.
+    AttestationFailed,
+    /// VM attestation is not supported in the current environment.
+    AttestationUnsupported,
+}
+
+impl Error for AttestationError {}
+
+impl Display for AttestationError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
+        let status = match self {
+            Self::InvalidChallenge => AVmAttestationStatus::ATTESTATION_ERROR_INVALID_CHALLENGE,
+            Self::AttestationFailed => AVmAttestationStatus::ATTESTATION_ERROR_ATTESTATION_FAILED,
+            Self::AttestationUnsupported => AVmAttestationStatus::ATTESTATION_ERROR_UNSUPPORTED,
+        };
+        // SAFETY: AVmAttestationStatus_toString always returns a non-null pointer to a
+        // nul-terminated C string with static lifetime (which is valid UTF-8).
+        let c_str = unsafe { CStr::from_ptr(AVmAttestationStatus_toString(status)) };
+        let str = c_str.to_str().expect("Invalid UTF-8 for AVmAttestationStatus");
+        f.write_str(str)
+    }
+}
+
+impl Drop for AttestationResult {
+    fn drop(&mut self) {
+        let ptr = self.result.as_ptr();
+
+        // SAFETY: The `result` field is private, and only populated with a successful call to
+        // `AVmPayload_requestAttestation`, and not freed elsewhere.
+        unsafe { AVmAttestationResult_free(ptr) };
+    }
+}
+
+// SAFETY: The API functions that accept the `AVmAttestationResult` pointer are all safe to call
+// from any thread, including `AVmAttestationResult_free` which is called only on drop.
+unsafe impl Send for AttestationResult {}
+
+// SAFETY: There is no interior mutation here; any future functions that might mutate the data would
+// require a non-const pointer and hence need `&mut self` here. The only existing such function is
+// `AVmAttestationResult_free` where we take a mutable reference guaranteeing no other references
+// exist. The raw API functions are safe to call from any thread.
+unsafe impl Sync for AttestationResult {}
+
+/// Requests the remote attestation of this VM.
+///
+/// On success the supplied [`challenge`] will be included in the certificate chain accessible from
+/// the [`AttestationResult`]; this can be used as proof of the freshness of the attestation.
+///
+/// The challenge should be no more than 64 bytes long or the request will fail.
+pub fn request_attestation(challenge: &[u8]) -> Result<AttestationResult, AttestationError> {
+    let mut result: *mut AVmAttestationResult = ptr::null_mut();
+    // SAFETY: We only read the challenge within its bounds and the function does not retain any
+    // reference to it.
+    let status = unsafe {
+        AVmPayload_requestAttestation(
+            challenge.as_ptr() as *const c_void,
+            challenge.len(),
+            &mut result,
+        )
+    };
+    AttestationResult::new(status, result)
+}
+
+/// A variant of [`request_attestation`] used for testing purposes. This should not be used by
+/// normal VMs, and is not available to app owned VMs.
+pub fn request_attestation_for_testing(
+    challenge: &[u8],
+) -> Result<AttestationResult, AttestationError> {
+    let mut result: *mut AVmAttestationResult = ptr::null_mut();
+    // SAFETY: 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 result,
+        )
+    };
+    AttestationResult::new(status, result)
+}
+
+impl AttestationResult {
+    fn new(
+        status: AVmAttestationStatus,
+        result: *mut AVmAttestationResult,
+    ) -> Result<AttestationResult, AttestationError> {
+        match status {
+            AVmAttestationStatus::ATTESTATION_ERROR_INVALID_CHALLENGE => {
+                Err(AttestationError::InvalidChallenge)
+            }
+            AVmAttestationStatus::ATTESTATION_ERROR_ATTESTATION_FAILED => {
+                Err(AttestationError::AttestationFailed)
+            }
+            AVmAttestationStatus::ATTESTATION_ERROR_UNSUPPORTED => {
+                Err(AttestationError::AttestationUnsupported)
+            }
+            AVmAttestationStatus::ATTESTATION_OK => {
+                let result = NonNull::new(result)
+                    .expect("Attestation succeeded but the attestation result is null");
+                Ok(AttestationResult { result })
+            }
+        }
+    }
+
+    fn as_const_ptr(&self) -> *const AVmAttestationResult {
+        self.result.as_ptr().cast_const()
+    }
+
+    /// Returns the attested private key. This is the ECDSA P-256 private key corresponding to the
+    /// public key described by the leaf certificate in the attested
+    /// [certificate chain](AttestationResult::certificate_chain). It is a DER-encoded
+    /// `ECPrivateKey` structure as specified in
+    /// [RFC 5915 s3](https://datatracker.ietf.org/doc/html/rfc5915#section-3).
+    ///
+    /// Note: The [`sign_message`](AttestationResult::sign_message) method allows signing with the
+    /// key without retrieving it.
+    pub fn private_key(&self) -> Vec<u8> {
+        let ptr = self.as_const_ptr();
+
+        let size =
+            // SAFETY: We own the `AVmAttestationResult` pointer, so it is valid. The function
+            // writes no data since we pass a zero size, and null is explicitly allowed for the
+            // destination in that case.
+            unsafe { AVmAttestationResult_getPrivateKey(ptr, ptr::null_mut(), 0) };
+
+        let mut private_key = vec![0u8; size];
+        // SAFETY: We own the `AVmAttestationResult` pointer, so it is valid. The function only
+        // writes within the bounds of `private_key`, which we just allocated so cannot be aliased.
+        let size = unsafe {
+            AVmAttestationResult_getPrivateKey(
+                ptr,
+                private_key.as_mut_ptr() as *mut c_void,
+                private_key.len(),
+            )
+        };
+        assert_eq!(size, private_key.len());
+        private_key
+    }
+
+    /// Signs the given message using the attested private key. The signature uses ECDSA P-256; the
+    /// message is first hashed with SHA-256 and then it is signed with the attested EC P-256
+    /// [private key](AttestationResult::private_key).
+    ///
+    /// The signature is a DER-encoded `ECDSASignature`` structure as described in
+    /// [RFC 6979](https://datatracker.ietf.org/doc/html/rfc6979).
+    pub fn sign_message(&self, message: &[u8]) -> Vec<u8> {
+        let ptr = self.as_const_ptr();
+
+        // SAFETY: We own the `AVmAttestationResult` pointer, so it is valid. The function
+        // writes no data since we pass a zero size, and null is explicitly allowed for the
+        // destination in that case.
+        let size = unsafe {
+            AVmAttestationResult_sign(
+                ptr,
+                message.as_ptr() as *const c_void,
+                message.len(),
+                ptr::null_mut(),
+                0,
+            )
+        };
+
+        let mut signature = vec![0u8; size];
+        // SAFETY: We own the `AVmAttestationResult` pointer, so it is valid. The function only
+        // writes within the bounds of `signature`, which we just allocated so cannot be aliased.
+        let size = unsafe {
+            AVmAttestationResult_sign(
+                ptr,
+                message.as_ptr() as *const c_void,
+                message.len(),
+                signature.as_mut_ptr() as *mut c_void,
+                signature.len(),
+            )
+        };
+        assert!(size <= signature.len());
+        signature.truncate(size);
+        signature
+    }
+
+    /// Returns an iterator over the certificates forming the certificate chain for the VM, and its
+    /// public key, obtained by the attestation process.
+    ///
+    /// The certificate chain consists of a sequence of DER-encoded X.509 certificates that form
+    /// the attestation key's certificate chain. It starts with the leaf certificate covering the
+    /// attested public key and ends with the root certificate.
+    pub fn certificate_chain(&self) -> CertIterator {
+        // SAFETY: We own the `AVmAttestationResult` pointer, so it is valid.
+        let count = unsafe { AVmAttestationResult_getCertificateCount(self.as_const_ptr()) };
+
+        CertIterator { result: self, count, current: 0 }
+    }
+
+    fn certificate(&self, index: usize) -> Vec<u8> {
+        let ptr = self.as_const_ptr();
+
+        let size =
+            // SAFETY: We own the `AVmAttestationResult` pointer, so it is valid. The function
+            // writes no data since we pass a zero size, and null is explicitly allowed for the
+            // destination in that case. The function will panic if `index` is out of range (which
+            // is safe).
+            unsafe { AVmAttestationResult_getCertificateAt(ptr, index, ptr::null_mut(), 0) };
+
+        let mut cert = vec![0u8; size];
+        // SAFETY: We own the `AVmAttestationResult` pointer, so it is valid. The function only
+        // writes within the bounds of `cert`, which we just allocated so cannot be aliased.
+        let size = unsafe {
+            AVmAttestationResult_getCertificateAt(
+                ptr,
+                index,
+                cert.as_mut_ptr() as *mut c_void,
+                cert.len(),
+            )
+        };
+        assert_eq!(size, cert.len());
+        cert
+    }
+}
+
+/// An iterator over the DER-encoded X.509 certificates containin in an [`AttestationResult`].
+/// See [`certificate_chain`](AttestationResult::certificate_chain) for more details.
+pub struct CertIterator<'a> {
+    result: &'a AttestationResult,
+    count: usize,
+    current: usize, // Invariant: current <= count
+}
+
+impl<'a> Iterator for CertIterator<'a> {
+    type Item = Vec<u8>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.current < self.count {
+            let cert = self.result.certificate(self.current);
+            self.current += 1;
+            Some(cert)
+        } else {
+            None
+        }
+    }
+
+    fn size_hint(&self) -> (usize, Option<usize>) {
+        let size = self.count - self.current;
+        (size, Some(size))
+    }
+}
+
+impl<'a> ExactSizeIterator for CertIterator<'a> {}
+impl<'a> FusedIterator for CertIterator<'a> {}
diff --git a/vm_payload/wrapper/lib.rs b/vm_payload/wrapper/lib.rs
new file mode 100644
index 0000000..d3f03d7
--- /dev/null
+++ b/vm_payload/wrapper/lib.rs
@@ -0,0 +1,196 @@
+/*
+ * 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.
+ */
+
+//! Rust wrapper for the VM Payload API, allowing virtual machine payload code to be written in
+//! Rust. This wraps the raw C API, accessed via bindgen, into a more idiomatic Rust interface.
+//!
+//! See `https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Virtualization/vm_payload/README.md`
+//! for more information on the VM Payload API.
+
+mod attestation;
+
+pub use attestation::{request_attestation, AttestationError, AttestationResult};
+use binder::unstable_api::AsNative;
+use binder::{FromIBinder, Strong};
+use std::ffi::{c_void, CStr, OsStr};
+use std::os::unix::ffi::OsStrExt;
+use std::path::Path;
+use std::ptr;
+use vm_payload_bindgen::{
+    AIBinder, AVmPayload_getApkContentsPath, AVmPayload_getEncryptedStoragePath,
+    AVmPayload_getVmInstanceSecret, AVmPayload_notifyPayloadReady, AVmPayload_runVsockRpcServer,
+};
+
+/// The functions declared here are restricted to VMs created with a config file;
+/// they will fail, or panic, if called in other VMs. The ability to create such VMs
+/// requires the android.permission.USE_CUSTOM_VIRTUAL_MACHINE permission, and is
+/// therefore not available to privileged or third party apps.
+///
+/// These functions can be used by tests, if the permission is granted via shell.
+pub mod restricted {
+    pub use crate::attestation::request_attestation_for_testing;
+}
+
+/// Marks the main function of the VM payload.
+///
+/// When the VM is run, this function is called. If it returns, the VM ends normally with a 0 exit
+/// code.
+///
+/// Example:
+///
+/// ```rust
+/// use log::info;
+///
+/// vm_payload::main!(vm_main);
+///
+/// fn vm_main() {
+///     android_logger::init_once(
+///          android_logger::Config::default()
+///             .with_tag("example_vm_payload")
+///             .with_max_level(log::LevelFilter::Info),
+///     );
+///     info!("Hello world");
+/// }
+/// ```
+#[macro_export]
+macro_rules! main {
+    ($name:path) => {
+        // Export a symbol with a name matching the extern declaration below.
+        #[export_name = "rust_main"]
+        fn __main() {
+            // Ensure that the main function provided by the application has the correct type.
+            $name()
+        }
+    };
+}
+
+// This is the real C entry point for the VM; we just forward to the Rust entry point.
+#[allow(non_snake_case)]
+#[no_mangle]
+extern "C" fn AVmPayload_main() {
+    extern "Rust" {
+        fn rust_main();
+    }
+
+    // SAFETY: rust_main is provided by the application using the `main!` macro above, which makes
+    // sure it has the right type.
+    unsafe { rust_main() }
+}
+
+/// Notifies the host that the payload is ready.
+///
+/// If the host app has set a `VirtualMachineCallback` for the VM, its
+/// `onPayloadReady` method will be called.
+///
+/// Note that subsequent calls to this function after the first have no effect;
+/// `onPayloadReady` is never called more than once.
+pub fn notify_payload_ready() {
+    // SAFETY: Invokes a method from the bindgen library `vm_payload_bindgen` which is safe to
+    // call at any time.
+    unsafe { AVmPayload_notifyPayloadReady() };
+}
+
+/// Runs a binder RPC server, serving the supplied binder service implementation on the given vsock
+/// port.
+///
+/// If and when the server is ready for connections (i.e. it is listening on the port),
+/// [`notify_payload_ready`] is called to notify the host that the server is ready. This is
+/// appropriate for VM payloads that serve a single binder service - which is common.
+///
+/// Note that this function does not return. The calling thread joins the binder
+/// thread pool to handle incoming messages.
+pub fn run_single_vsock_service<T>(service: Strong<T>, port: u32) -> !
+where
+    T: FromIBinder + ?Sized,
+{
+    extern "C" fn on_ready(_param: *mut c_void) {
+        notify_payload_ready();
+    }
+
+    let mut service = service.as_binder();
+    // The cast here is needed because the compiler doesn't know that our vm_payload_bindgen
+    // AIBinder is the same type as binder_ndk_sys::AIBinder.
+    let service = service.as_native_mut() as *mut AIBinder;
+    let param = ptr::null_mut();
+    // SAFETY: We have a strong reference to the service, so the raw pointer remains valid. It is
+    // safe for on_ready to be invoked at any time, with any parameter.
+    unsafe { AVmPayload_runVsockRpcServer(service, port, Some(on_ready), param) }
+}
+
+/// Gets the path to the contents of the APK containing the VM payload. It is a directory, under
+/// which are the unzipped contents of the APK containing the payload, all read-only
+/// but accessible to the payload.
+pub fn apk_contents_path() -> &'static Path {
+    // SAFETY: AVmPayload_getApkContentsPath always returns a non-null pointer to a
+    // nul-terminated C string with static lifetime.
+    let c_str = unsafe { CStr::from_ptr(AVmPayload_getApkContentsPath()) };
+    Path::new(OsStr::from_bytes(c_str.to_bytes()))
+}
+
+/// Gets the path to the encrypted persistent storage for the VM, if any. This is
+/// a directory under which any files or directories created will be stored on
+/// behalf of the VM by the host app. All data is encrypted using a key known
+/// only to the VM, so the host cannot decrypt it, but may delete it.
+///
+/// Returns `None` if no encrypted storage was requested in the VM configuration.
+pub fn encrypted_storage_path() -> Option<&'static Path> {
+    // SAFETY: AVmPayload_getEncryptedStoragePath returns either null or a pointer to a
+    // nul-terminated C string with static lifetime.
+    let ptr = unsafe { AVmPayload_getEncryptedStoragePath() };
+    if ptr.is_null() {
+        None
+    } else {
+        // SAFETY: We know the pointer is not null, and so it is a valid C string.
+        let c_str = unsafe { CStr::from_ptr(ptr) };
+        Some(Path::new(OsStr::from_bytes(c_str.to_bytes())))
+    }
+}
+
+/// Retrieves all or part of a 32-byte secret that is bound to this unique VM
+/// instance and the supplied identifier. The secret can be used e.g. as an
+/// encryption key.
+///
+/// Every VM has a secret that is derived from a device-specific value known to
+/// the hypervisor, the code that runs in the VM and its non-modifiable
+/// configuration; it is not made available to the host OS.
+///
+/// This function performs a further derivation from the VM secret and the
+/// supplied identifier. As long as the VM identity doesn't change the same value
+/// will be returned for the same identifier, even if the VM is stopped &
+/// restarted or the device rebooted.
+///
+/// If multiple secrets are required for different purposes, a different
+/// identifier should be used for each. The identifiers otherwise are arbitrary
+/// byte sequences and do not need to be kept secret; typically they are
+/// hardcoded in the calling code.
+///
+/// The secret is returned in [`secret`], truncated to its size, which must be between
+/// 1 and 32 bytes (inclusive) or the function will panic.
+pub fn get_vm_instance_secret(identifier: &[u8], secret: &mut [u8]) {
+    let secret_size = secret.len();
+    assert!((1..=32).contains(&secret_size), "VM instance secrets can be up to 32 bytes long");
+
+    // SAFETY: The function only reads from `[identifier]` within its bounds, and only writes to
+    // `[secret]` within its bounds. Neither reference is retained, and we know neither is null.
+    unsafe {
+        AVmPayload_getVmInstanceSecret(
+            identifier.as_ptr() as *const c_void,
+            identifier.len(),
+            secret.as_mut_ptr() as *mut c_void,
+            secret_size,
+        )
+    }
+}
diff --git a/vmbase/Android.bp b/vmbase/Android.bp
index f01e8aa..ee12e85 100644
--- a/vmbase/Android.bp
+++ b/vmbase/Android.bp
@@ -57,6 +57,8 @@
         hwaddress: false,
     },
     native_coverage: false,
+    // TODO(b/346974429): Workaround pvmfw failure when enabling full LTO
+    lto_O0: true,
 }
 
 // Used by cc_binary when producing the ELF of a vmbase-based binary.
diff --git a/vmbase/src/uart.rs b/vmbase/src/uart.rs
index 09d747f..e35555d 100644
--- a/vmbase/src/uart.rs
+++ b/vmbase/src/uart.rs
@@ -16,7 +16,6 @@
 //! provided by crosvm, and won't work with real hardware.
 
 use core::fmt::{self, Write};
-use core::ptr::write_volatile;
 
 /// Minimal driver for an 8250 UART. This only implements enough to work with the emulated 8250
 /// provided by crosvm, and won't work with real hardware.
@@ -41,7 +40,11 @@
         // SAFETY: We know that the base address points to the control registers of a UART device
         // which is appropriately mapped.
         unsafe {
-            write_volatile(self.base_address, byte);
+            core::arch::asm!(
+                "strb {value:w}, [{ptr}]",
+                value = in(reg) byte,
+                ptr = in(reg) self.base_address,
+            );
         }
     }
 }
diff --git a/vmbase/src/virtio/hal.rs b/vmbase/src/virtio/hal.rs
index 0d3f445..52635c3 100644
--- a/vmbase/src/virtio/hal.rs
+++ b/vmbase/src/virtio/hal.rs
@@ -91,7 +91,7 @@
         let bounce = alloc_shared(bb_layout(size))
             .expect("Failed to allocate and share VirtIO bounce buffer with host");
         let paddr = virt_to_phys(bounce);
-        if direction == BufferDirection::DriverToDevice {
+        if direction != BufferDirection::DeviceToDriver {
             let src = buffer.cast::<u8>().as_ptr().cast_const();
             trace!("VirtIO bounce buffer at {bounce:?} (PA:{paddr:#x}) initialized from {src:?}");
             // SAFETY: Both regions are valid, properly aligned, and don't overlap.
@@ -104,7 +104,7 @@
     unsafe fn unshare(paddr: PhysAddr, buffer: NonNull<[u8]>, direction: BufferDirection) {
         let bounce = phys_to_virt(paddr);
         let size = buffer.len();
-        if direction == BufferDirection::DeviceToDriver {
+        if direction != BufferDirection::DriverToDevice {
             let dest = buffer.cast::<u8>().as_ptr();
             trace!("VirtIO bounce buffer at {bounce:?} (PA:{paddr:#x}) copied back to {dest:?}");
             // SAFETY: Both regions are valid, properly aligned, and don't overlap.
diff --git a/vmlauncher_app/AndroidManifest.xml b/vmlauncher_app/AndroidManifest.xml
index d800ec7..bae3227 100644
--- a/vmlauncher_app/AndroidManifest.xml
+++ b/vmlauncher_app/AndroidManifest.xml
@@ -7,19 +7,27 @@
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-feature android:name="android.software.virtualization_framework" android:required="true" />
     <application
-        android:label="VmLauncherApp"
-        android:networkSecurityConfig="@xml/network_security_config">
+        android:label="VmLauncherApp">
         <activity android:name=".MainActivity"
-                  android:enabled="false"
                   android:screenOrientation="landscape"
                   android:configChanges="orientation|screenSize|keyboard|keyboardHidden|navigation|uiMode"
                   android:theme="@style/MyTheme"
                   android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
+                <action android:name="android.virtualization.VM_LAUNCHER" />
+                <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity>
+        <activity-alias android:name=".MainActivityAlias"
+                android:targetActivity="com.android.virtualization.vmlauncher.MainActivity"
+                android:exported="true"
+                android:enabled="false">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity-alias>
     </application>
 
 </manifest>
diff --git a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
index 10f8bf6..c2f218a 100644
--- a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
+++ b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
@@ -19,42 +19,52 @@
 import static android.system.virtualmachine.VirtualMachineConfig.CPU_TOPOLOGY_MATCH_HOST;
 
 import android.app.Activity;
+import android.crosvm.ICrosvmAndroidDisplayService;
+import android.graphics.PixelFormat;
 import android.graphics.Rect;
 import android.os.Bundle;
+import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.os.ServiceManager;
-import android.crosvm.ICrosvmAndroidDisplayService;
 import android.system.virtualizationservice_internal.IVirtualizationServiceInternal;
-import android.system.virtualmachine.VirtualMachineCustomImageConfig;
-import android.system.virtualmachine.VirtualMachineCustomImageConfig.DisplayConfig;
-import android.util.DisplayMetrics;
-import android.util.Log;
 import android.system.virtualmachine.VirtualMachine;
 import android.system.virtualmachine.VirtualMachineCallback;
 import android.system.virtualmachine.VirtualMachineConfig;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig.DisplayConfig;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig.GpuConfig;
 import android.system.virtualmachine.VirtualMachineException;
 import android.system.virtualmachine.VirtualMachineManager;
+import android.util.DisplayMetrics;
+import android.util.Log;
 import android.view.Display;
 import android.view.InputDevice;
+import android.view.KeyEvent;
 import android.view.SurfaceHolder;
 import android.view.SurfaceView;
-import android.view.KeyEvent;
 import android.view.View;
-import android.view.WindowManager;
 import android.view.WindowInsets;
 import android.view.WindowInsetsController;
+import android.view.WindowManager;
 import android.view.WindowMetrics;
 
+import libcore.io.IoBridge;
+
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
+import java.io.BufferedOutputStream;
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -63,8 +73,9 @@
     private static final String TAG = "VmLauncherApp";
     private static final String VM_NAME = "my_custom_vm";
     private static final boolean DEBUG = true;
-    private final ExecutorService mExecutorService = Executors.newFixedThreadPool(4);
+    private ExecutorService mExecutorService;
     private VirtualMachine mVirtualMachine;
+    private ParcelFileDescriptor mCursorStream;
 
     private VirtualMachineConfig createVirtualMachineConfig(String jsonPath) {
         VirtualMachineConfig.Builder configBuilder =
@@ -75,6 +86,7 @@
         if (DEBUG) {
             configBuilder.setDebugLevel(VirtualMachineConfig.DEBUG_LEVEL_FULL);
             configBuilder.setVmOutputCaptured(true);
+            configBuilder.setConnectVmConsole(true);
         }
         VirtualMachineCustomImageConfig.Builder customImageConfigBuilder =
                 new VirtualMachineCustomImageConfig.Builder();
@@ -112,6 +124,50 @@
                     }
                 }
             }
+            if (json.has("console_input_device")) {
+                configBuilder.setConsoleInputDevice(json.getString("console_input_device"));
+            }
+            if (json.has("gpu")) {
+                JSONObject gpuJson = json.getJSONObject("gpu");
+
+                GpuConfig.Builder gpuConfigBuilder = new GpuConfig.Builder();
+
+                if (gpuJson.has("backend")) {
+                    gpuConfigBuilder.setBackend(gpuJson.getString("backend"));
+                }
+                if (gpuJson.has("context_types")) {
+                    ArrayList<String> contextTypes = new ArrayList<String>();
+                    JSONArray contextTypesJson = gpuJson.getJSONArray("context_types");
+                    for (int i = 0; i < contextTypesJson.length(); i++) {
+                        contextTypes.add(contextTypesJson.getString(i));
+                    }
+                    gpuConfigBuilder.setContextTypes(contextTypes.toArray(new String[0]));
+                }
+                if (gpuJson.has("pci_address")) {
+                    gpuConfigBuilder.setPciAddress(gpuJson.getString("pci_address"));
+                }
+                if (gpuJson.has("renderer_features")) {
+                    gpuConfigBuilder.setRendererFeatures(gpuJson.getString("renderer_features"));
+                }
+                if (gpuJson.has("renderer_use_egl")) {
+                    gpuConfigBuilder.setRendererUseEgl(gpuJson.getBoolean("renderer_use_egl"));
+                }
+                if (gpuJson.has("renderer_use_gles")) {
+                    gpuConfigBuilder.setRendererUseGles(gpuJson.getBoolean("renderer_use_gles"));
+                }
+                if (gpuJson.has("renderer_use_glx")) {
+                    gpuConfigBuilder.setRendererUseGlx(gpuJson.getBoolean("renderer_use_glx"));
+                }
+                if (gpuJson.has("renderer_use_surfaceless")) {
+                    gpuConfigBuilder.setRendererUseSurfaceless(
+                            gpuJson.getBoolean("renderer_use_surfaceless"));
+                }
+                if (gpuJson.has("renderer_use_vulkan")) {
+                    gpuConfigBuilder.setRendererUseVulkan(
+                            gpuJson.getBoolean("renderer_use_vulkan"));
+                }
+                customImageConfigBuilder.setGpuConfig(gpuConfigBuilder.build());
+            }
 
             configBuilder.setMemoryBytes(8L * 1024 * 1024 * 1024 /* 8 GB */);
             WindowMetrics windowMetrics = getWindowManager().getCurrentWindowMetrics();
@@ -132,6 +188,7 @@
             customImageConfigBuilder.useTouch(true);
             customImageConfigBuilder.useKeyboard(true);
             customImageConfigBuilder.useMouse(true);
+            customImageConfigBuilder.useNetwork(true);
 
             configBuilder.setCustomImageConfig(customImageConfigBuilder.build());
 
@@ -160,6 +217,7 @@
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        mExecutorService = Executors.newCachedThreadPool();
         try {
             // To ensure that the previous display service is removed.
             IVirtualizationServiceInternal.Stub.asInterface(
@@ -239,14 +297,19 @@
             if (DEBUG) {
                 InputStream console = mVirtualMachine.getConsoleOutput();
                 InputStream log = mVirtualMachine.getLogOutput();
-                mExecutorService.execute(new Reader("console", console));
+                OutputStream consoleLogFile =
+                        new LineBufferedOutputStream(
+                                getApplicationContext().openFileOutput("console.log", 0));
+                mExecutorService.execute(new CopyStreamTask("console", console, consoleLogFile));
                 mExecutorService.execute(new Reader("log", log));
             }
-        } catch (VirtualMachineException e) {
+        } catch (VirtualMachineException | IOException e) {
             throw new RuntimeException(e);
         }
 
         SurfaceView surfaceView = findViewById(R.id.surface_view);
+        SurfaceView cursorSurfaceView = findViewById(R.id.cursor_surface_view);
+        cursorSurfaceView.setZOrderMediaOverlay(true);
         View backgroundTouchView = findViewById(R.id.background_touch_view);
         backgroundTouchView.setOnTouchListener(
                 (v, event) -> {
@@ -280,7 +343,10 @@
                                                 + holder.getSurface()
                                                 + ")");
                                 runWithDisplayService(
-                                        (service) -> service.setSurface(holder.getSurface()));
+                                        (service) ->
+                                                service.setSurface(
+                                                        holder.getSurface(),
+                                                        false /* forCursor */));
                             }
 
                             @Override
@@ -292,7 +358,52 @@
                             @Override
                             public void surfaceDestroyed(SurfaceHolder holder) {
                                 Log.d(TAG, "ICrosvmAndroidDisplayService.removeSurface()");
-                                runWithDisplayService((service) -> service.removeSurface());
+                                runWithDisplayService(
+                                        (service) -> service.removeSurface(false /* forCursor */));
+                            }
+                        });
+        cursorSurfaceView.getHolder().setFormat(PixelFormat.RGBA_8888);
+        cursorSurfaceView
+                .getHolder()
+                .addCallback(
+                        new SurfaceHolder.Callback() {
+                            @Override
+                            public void surfaceCreated(SurfaceHolder holder) {
+                                try {
+                                    ParcelFileDescriptor[] pfds =
+                                            ParcelFileDescriptor.createSocketPair();
+                                    mExecutorService.execute(
+                                            new CursorHandler(cursorSurfaceView, pfds[0]));
+                                    mCursorStream = pfds[0];
+                                    runWithDisplayService(
+                                            (service) -> service.setCursorStream(pfds[1]));
+                                } catch (Exception e) {
+                                    Log.d("TAG", "failed to run cursor stream handler", e);
+                                }
+                                runWithDisplayService(
+                                        (service) ->
+                                                service.setSurface(
+                                                        holder.getSurface(), true /* forCursor */));
+                            }
+
+                            @Override
+                            public void surfaceChanged(
+                                    SurfaceHolder holder, int format, int width, int height) {
+                                Log.d(TAG, "width: " + width + ", height: " + height);
+                            }
+
+                            @Override
+                            public void surfaceDestroyed(SurfaceHolder holder) {
+                                Log.d(TAG, "ICrosvmAndroidDisplayService.removeSurface()");
+                                runWithDisplayService(
+                                        (service) -> service.removeSurface(true /* forCursor */));
+                                if (mCursorStream != null) {
+                                    try {
+                                        mCursorStream.close();
+                                    } catch (IOException e) {
+                                        Log.d(TAG, "failed to close fd", e);
+                                    }
+                                }
                             }
                         });
         getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
@@ -305,6 +416,15 @@
     }
 
     @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        if (mExecutorService != null) {
+            mExecutorService.shutdownNow();
+        }
+        Log.d(TAG, "destroyed");
+    }
+
+    @Override
     public void onWindowFocusChanged(boolean hasFocus) {
         super.onWindowFocusChanged(hasFocus);
         if (hasFocus) {
@@ -336,6 +456,43 @@
         }
     }
 
+    static class CursorHandler implements Runnable {
+        private final SurfaceView mSurfaceView;
+        private final ParcelFileDescriptor mStream;
+
+        CursorHandler(SurfaceView s, ParcelFileDescriptor stream) {
+            mSurfaceView = s;
+            mStream = stream;
+        }
+
+        @Override
+        public void run() {
+            Log.d(TAG, "CursorHandler");
+            try {
+                ByteBuffer byteBuffer = ByteBuffer.allocate(8 /* (x: u32, y: u32) */);
+                byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+                while (true) {
+                    byteBuffer.clear();
+                    int bytes =
+                            IoBridge.read(
+                                    mStream.getFileDescriptor(),
+                                    byteBuffer.array(),
+                                    0,
+                                    byteBuffer.array().length);
+                    float x = (float) (byteBuffer.getInt() & 0xFFFFFFFF);
+                    float y = (float) (byteBuffer.getInt() & 0xFFFFFFFF);
+                    mSurfaceView.post(
+                            () -> {
+                                mSurfaceView.setTranslationX(x);
+                                mSurfaceView.setTranslationY(y);
+                            });
+                }
+            } catch (IOException e) {
+                Log.e(TAG, e.getMessage());
+            }
+        }
+    }
+
     /** Reads data from an input stream and posts it to the output data */
     static class Reader implements Runnable {
         private final String mName;
@@ -359,4 +516,49 @@
             }
         }
     }
+
+    private static class CopyStreamTask implements Runnable {
+        private final String mName;
+        private final InputStream mIn;
+        private final OutputStream mOut;
+
+        CopyStreamTask(String name, InputStream in, OutputStream out) {
+            mName = name;
+            mIn = in;
+            mOut = out;
+        }
+
+        @Override
+        public void run() {
+            try {
+                byte[] buffer = new byte[2048];
+                while (!Thread.interrupted()) {
+                    int len = mIn.read(buffer);
+                    if (len < 0) {
+                        break;
+                    }
+                    mOut.write(buffer, 0, len);
+                }
+            } catch (Exception e) {
+                Log.e(TAG, "Exception while posting " + mName, e);
+            }
+        }
+    }
+
+    private static class LineBufferedOutputStream extends BufferedOutputStream {
+        LineBufferedOutputStream(OutputStream out) {
+            super(out);
+        }
+
+        @Override
+        public void write(byte[] buf, int off, int len) throws IOException {
+            super.write(buf, off, len);
+            for (int i = 0; i < len; ++i) {
+                if (buf[off + i] == '\n') {
+                    flush();
+                    break;
+                }
+            }
+        }
+    }
 }
diff --git a/vmlauncher_app/res/layout/activity_main.xml b/vmlauncher_app/res/layout/activity_main.xml
index e52dfcd..a80ece0 100644
--- a/vmlauncher_app/res/layout/activity_main.xml
+++ b/vmlauncher_app/res/layout/activity_main.xml
@@ -11,7 +11,7 @@
       android:layout_height="match_parent"
     />
   <SurfaceView
-        android:id="@+id/surface_view"
+      android:id="@+id/surface_view"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:focusable="true"
@@ -20,5 +20,11 @@
       android:defaultFocusHighlightEnabled="true">
     <requestFocus />
   </SurfaceView>
+  <!-- A cursor size in virtio-gpu spec is always 64x64 -->
+  <SurfaceView
+      android:id="@+id/cursor_surface_view"
+      android:layout_width="64px"
+      android:layout_height="64px">
+  </SurfaceView>
 
 </merge>
diff --git a/vmlauncher_app/res/xml/network_security_config.xml b/vmlauncher_app/res/xml/network_security_config.xml
deleted file mode 100644
index f27fa56..0000000
--- a/vmlauncher_app/res/xml/network_security_config.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?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.
-  -->
-
-<network-security-config>
-    <domain-config cleartextTrafficPermitted="true">
-        <domain includeSubdomains="true">localhost</domain>
-    </domain-config>
-</network-security-config>