Merge "Microdroid: Map a dm-crypt dev on (virtio-blk)disk"
diff --git a/OWNERS b/OWNERS
index ecd24ed..310add7 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,5 +1,7 @@
 # Welcome to Android KVM!
 #
+# Bug component: 867125
+#
 # If you are not a member of the project please send review requests
 # to one of those listed below.
 dbrazdil@google.com
diff --git a/apex/Android.bp b/apex/Android.bp
index e0ca9bf..4e64e50 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -15,20 +15,58 @@
     "microdroid_vendor_boot",
 ]
 
-apex {
-    name: "com.android.virt",
+soong_config_module_type {
+    name: "virt_apex",
+    module_type: "apex",
+    config_namespace: "ANDROID",
+    bool_variables: ["avf_enabled"],
+    properties: ["defaults"],
+}
 
+virt_apex {
+    name: "com.android.virt",
+    soong_config_variables: {
+        avf_enabled: {
+            defaults: ["com.android.virt_avf_enabled"],
+            conditions_default: {
+                defaults: ["com.android.virt_avf_disabled"],
+            },
+        },
+    },
+}
+
+apex_defaults {
+    name: "com.android.virt_common",
     // TODO(jiyong): make it updatable
     updatable: false,
     future_updatable: true,
     platform_apis: true,
 
-    system_ext_specific: true,
-
     manifest: "manifest.json",
 
     key: "com.android.virt.key",
     certificate: ":com.android.virt.certificate",
+
+    apps: [
+        "android.system.virtualmachine.res",
+    ],
+
+    file_contexts: ":com.android.virt-file_contexts",
+    canned_fs_config: "canned_fs_config",
+
+    bootclasspath_fragments: [
+        "com.android.virt-bootclasspath-fragment",
+    ],
+    jni_libs: [
+        "libvirtualmachine_jni",
+    ],
+}
+
+apex_defaults {
+    name: "com.android.virt_avf_enabled",
+
+    defaults: ["com.android.virt_common"],
+
     custom_sign_tool: "sign_virt_apex",
 
     // crosvm and virtualizationservice are only enabled for 64-bit targets on device
@@ -52,17 +90,9 @@
         "fd_server",
         "vm",
     ],
-    java_libs: [
-        "android.system.virtualmachine",
-    ],
-    jni_libs: [
-        "libvirtualmachine_jni",
-    ],
-    apps: [
-        "android.system.virtualmachine.res",
-    ],
     prebuilts: [
         "com.android.virt.init.rc",
+        "features_com.android.virt.xml",
         "microdroid_initrd_app_debuggable",
         "microdroid_initrd_full_debuggable",
         "microdroid_initrd_normal",
@@ -71,13 +101,17 @@
         "microdroid_bootloader.avbpubkey",
         "microdroid_kernel",
     ],
-    file_contexts: ":com.android.virt-file_contexts",
-    canned_fs_config: "canned_fs_config",
     host_required: [
         "vm_shell",
     ],
 }
 
+apex_defaults {
+    name: "com.android.virt_avf_disabled",
+
+    defaults: ["com.android.virt_common"],
+}
+
 apex_key {
     name: "com.android.virt.key",
     public_key: "com.android.virt.avbpubkey",
@@ -174,3 +208,43 @@
         },
     },
 }
+
+// Encapsulate the contributions made by the com.android.virt to the bootclasspath.
+bootclasspath_fragment {
+    name: "com.android.virt-bootclasspath-fragment",
+    contents: ["framework-virtualization"],
+    apex_available: ["com.android.virt"],
+
+    // The bootclasspath_fragments that provide APIs on which this depends.
+    fragments: [
+        {
+            apex: "com.android.art",
+            module: "art-bootclasspath-fragment",
+        },
+    ],
+
+    // Additional stubs libraries that this fragment's contents use which are
+    // not provided by another bootclasspath_fragment.
+    additional_stubs: [
+        "android-non-updatable",
+    ],
+
+    hidden_api: {
+
+        // This module does not contain any split packages.
+        split_packages: [],
+
+        // The following packages and all their subpackages currently only
+        // contain classes from this bootclasspath_fragment. Listing a package
+        // here won't prevent other bootclasspath modules from adding classes in
+        // any of those packages but it will prevent them from adding those
+        // classes into an API surface, e.g. public, system, etc.. Doing so will
+        // result in a build failure due to inconsistent flags.
+        package_prefixes: [
+            "android.system.virtualmachine",
+            "android.system.virtualizationservice",
+            // android.sysprop.*, renamed by jarjar
+            "com.android.system.virtualmachine.sysprop",
+        ],
+    },
+}
diff --git a/apex/permissions/Android.bp b/apex/permissions/Android.bp
new file mode 100644
index 0000000..0c925ce
--- /dev/null
+++ b/apex/permissions/Android.bp
@@ -0,0 +1,24 @@
+//
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+prebuilt_etc {
+    name: "features_com.android.virt.xml",
+    sub_dir: "permissions",
+    src: "features_com.android.virt.xml",
+}
diff --git a/apex/permissions/features_com.android.virt.xml b/apex/permissions/features_com.android.virt.xml
new file mode 100644
index 0000000..d2b32e6
--- /dev/null
+++ b/apex/permissions/features_com.android.virt.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<permissions>
+    <feature name="android.software.virtualization_framework" />
+</permissions>
diff --git a/apex/product_packages.mk b/apex/product_packages.mk
index 5bc0044..4293c80 100644
--- a/apex/product_packages.mk
+++ b/apex/product_packages.mk
@@ -21,7 +21,6 @@
 
 PRODUCT_PACKAGES += \
     com.android.compos \
-    com.android.virt \
 
 # TODO(b/207336449): Figure out how to get these off /system
 PRODUCT_ARTIFACT_PATH_REQUIREMENT_ALLOWED_LIST := \
@@ -33,3 +32,5 @@
 PRODUCT_SYSTEM_EXT_PROPERTIES := ro.config.isolated_compilation_enabled=true
 
 PRODUCT_FSVERITY_GENERATE_METADATA := true
+
+PRODUCT_AVF_ENABLED := true
diff --git a/apkdmverity/src/main.rs b/apkdmverity/src/main.rs
index a69b583..6e12e38 100644
--- a/apkdmverity/src/main.rs
+++ b/apkdmverity/src/main.rs
@@ -162,7 +162,7 @@
     }
 
     fn create_block_aligned_file(path: &Path, data: &[u8]) {
-        let mut f = File::create(&path).unwrap();
+        let mut f = File::create(path).unwrap();
         f.write_all(data).unwrap();
 
         // Add padding so that the size of the file is multiple of 4096.
diff --git a/authfs/aidl/com/android/virt/fs/IAuthFsService.aidl b/authfs/aidl/com/android/virt/fs/IAuthFsService.aidl
index b349db2..30cc281 100644
--- a/authfs/aidl/com/android/virt/fs/IAuthFsService.aidl
+++ b/authfs/aidl/com/android/virt/fs/IAuthFsService.aidl
@@ -21,6 +21,8 @@
 
 /** @hide */
 interface IAuthFsService {
+    const String AUTHFS_SERVICE_SOCKET_NAME = "authfs_service";
+
     /**
      * Creates an AuthFS mount given the config. Returns the binder object that represent the AuthFS
      * instance. The AuthFS setup is deleted once the lifetime of the returned binder object ends.
diff --git a/authfs/service/Android.bp b/authfs/service/Android.bp
index e9eec1e..de6326d 100644
--- a/authfs/service/Android.bp
+++ b/authfs/service/Android.bp
@@ -16,6 +16,7 @@
         "liblibc",
         "liblog_rust",
         "libnix",
+        "librpcbinder_rs",
         "libshared_child",
     ],
     prefer_rlib: true,
diff --git a/authfs/service/authfs_service.rc b/authfs/service/authfs_service.rc
index 9ad0ce6..7edb1ca 100644
--- a/authfs/service/authfs_service.rc
+++ b/authfs/service/authfs_service.rc
@@ -1,2 +1,3 @@
 service authfs_service /system/bin/authfs_service
     disabled
+    socket authfs_service stream 0666 root system
diff --git a/authfs/service/src/main.rs b/authfs/service/src/main.rs
index 77cac9a..671c06a 100644
--- a/authfs/service/src/main.rs
+++ b/authfs/service/src/main.rs
@@ -22,8 +22,9 @@
 
 mod authfs;
 
-use anyhow::{bail, Context, Result};
+use anyhow::{bail, Result};
 use log::*;
+use rpcbinder::run_init_unix_domain_rpc_server;
 use std::ffi::OsString;
 use std::fs::{create_dir, read_dir, remove_dir_all, remove_file};
 use std::sync::atomic::{AtomicUsize, Ordering};
@@ -31,13 +32,10 @@
 use authfs_aidl_interface::aidl::com::android::virt::fs::AuthFsConfig::AuthFsConfig;
 use authfs_aidl_interface::aidl::com::android::virt::fs::IAuthFs::IAuthFs;
 use authfs_aidl_interface::aidl::com::android::virt::fs::IAuthFsService::{
-    BnAuthFsService, IAuthFsService,
+    BnAuthFsService, IAuthFsService, AUTHFS_SERVICE_SOCKET_NAME,
 };
-use binder::{
-    self, add_service, BinderFeatures, ExceptionCode, Interface, ProcessState, Status, Strong,
-};
+use binder::{self, BinderFeatures, ExceptionCode, Interface, Status, Strong};
 
-const SERVICE_NAME: &str = "authfs_service";
 const SERVICE_ROOT: &str = "/data/misc/authfs";
 
 /// Implementation of `IAuthFsService`.
@@ -117,15 +115,17 @@
 
     clean_up_working_directory()?;
 
-    ProcessState::start_thread_pool();
-
     let service = AuthFsService::new_binder(debuggable).as_binder();
-    add_service(SERVICE_NAME, service)
-        .with_context(|| format!("Failed to register service {}", SERVICE_NAME))?;
-    debug!("{} is running", SERVICE_NAME);
-
-    ProcessState::join_thread_pool();
-    bail!("Unexpected exit after join_thread_pool")
+    debug!("{} is starting as a rpc service.", AUTHFS_SERVICE_SOCKET_NAME);
+    let retval = run_init_unix_domain_rpc_server(service, AUTHFS_SERVICE_SOCKET_NAME, || {
+        info!("The RPC server '{}' is running.", AUTHFS_SERVICE_SOCKET_NAME);
+    });
+    if retval {
+        info!("The RPC server at '{}' has shut down gracefully.", AUTHFS_SERVICE_SOCKET_NAME);
+        Ok(())
+    } else {
+        bail!("Premature termination of the RPC server '{}'.", AUTHFS_SERVICE_SOCKET_NAME)
+    }
 }
 
 fn main() {
diff --git a/authfs/tests/benchmarks/src/java/com/android/fs/benchmarks/AuthFsBenchmarks.java b/authfs/tests/benchmarks/src/java/com/android/fs/benchmarks/AuthFsBenchmarks.java
index 8cee496..e67a309 100644
--- a/authfs/tests/benchmarks/src/java/com/android/fs/benchmarks/AuthFsBenchmarks.java
+++ b/authfs/tests/benchmarks/src/java/com/android/fs/benchmarks/AuthFsBenchmarks.java
@@ -28,12 +28,13 @@
 import android.platform.test.annotations.RootPermissionTest;
 
 import com.android.fs.common.AuthFsTestRule;
+import com.android.microdroid.test.common.DeviceProperties;
 import com.android.microdroid.test.common.MetricsProcessor;
-import com.android.microdroid.test.host.MicrodroidHostTestCaseBase;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.metrics.proto.MetricMeasurement.DataType;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Measurements;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
 
 import org.junit.After;
 import org.junit.AfterClass;
@@ -53,7 +54,7 @@
 @RootPermissionTest
 @RunWith(DeviceJUnit4Parameterized.class)
 @UseParametersRunnerFactory(DeviceJUnit4ClassRunnerWithParameters.RunnerFactory.class)
-public class AuthFsBenchmarks extends MicrodroidHostTestCaseBase {
+public class AuthFsBenchmarks extends BaseHostJUnit4Test {
     private static final int TRIAL_COUNT = 5;
 
     /** Name of the measure_io binary on host. */
@@ -83,10 +84,10 @@
         AuthFsTestRule.setUpAndroid(getTestInformation());
         mAuthFsTestRule.setUpTest();
         assumeTrue(AuthFsTestRule.getDevice().supportsMicrodroid(mProtectedVm));
-        assumeFalse("Skip on CF; protected VM not supported", isCuttlefish());
-        String metricsPrefix =
-                MetricsProcessor.getMetricPrefix(
-                        getDevice().getProperty("debug.hypervisor.metrics_tag"));
+        DeviceProperties deviceProperties = DeviceProperties.create(getDevice()::getProperty);
+        assumeFalse(
+                "Skip on CF; no need to collect metrics on CF", deviceProperties.isCuttlefish());
+        String metricsPrefix = MetricsProcessor.getMetricPrefix(deviceProperties.getMetricsTag());
         mMetricsProcessor = new MetricsProcessor(metricsPrefix + "authfs/");
         AuthFsTestRule.startMicrodroid(mProtectedVm);
     }
diff --git a/compos/common/compos_client.rs b/compos/common/compos_client.rs
index 68e1948..601c6fc 100644
--- a/compos/common/compos_client.rs
+++ b/compos/common/compos_client.rs
@@ -32,10 +32,8 @@
 use log::{info, warn};
 use rustutils::system_properties;
 use std::fs::{self, File};
-use std::io::{BufRead, BufReader};
 use std::num::NonZeroU32;
 use std::path::{Path, PathBuf};
-use std::thread;
 use vmclient::{DeathReason, ErrorCode, VmInstance, VmWaitError};
 
 /// This owns an instance of the CompOS VM.
@@ -240,12 +238,7 @@
 
 struct Callback {}
 impl vmclient::VmCallback for Callback {
-    fn on_payload_started(&self, cid: i32, stream: Option<&File>) {
-        if let Some(file) = stream {
-            if let Err(e) = start_logging(file) {
-                warn!("Can't log vm output: {}", e);
-            };
-        }
+    fn on_payload_started(&self, cid: i32) {
         log::info!("VM payload started, cid = {}", cid);
     }
 
@@ -265,19 +258,3 @@
         log::warn!("VM died, cid = {}, reason = {:?}", cid, death_reason);
     }
 }
-
-fn start_logging(file: &File) -> Result<()> {
-    let reader = BufReader::new(file.try_clone().context("Cloning file failed")?);
-    thread::spawn(move || {
-        for line in reader.lines() {
-            match line {
-                Ok(line) => info!("VM: {}", line),
-                Err(e) => {
-                    warn!("Reading VM output failed: {}", e);
-                    break;
-                }
-            }
-        }
-    });
-    Ok(())
-}
diff --git a/compos/src/artifact_signer.rs b/compos/src/artifact_signer.rs
index e51b8dd..d3843fc 100644
--- a/compos/src/artifact_signer.rs
+++ b/compos/src/artifact_signer.rs
@@ -46,7 +46,7 @@
     pub fn add_artifact(&mut self, path: &Path) -> Result<()> {
         // The path we store is where the file will be when it is verified, not where it is now.
         let suffix = path
-            .strip_prefix(&self.base_directory)
+            .strip_prefix(self.base_directory)
             .context("Artifacts must be under base directory")?;
         let target_path = Path::new(TARGET_DIRECTORY).join(suffix);
         let target_path = target_path.to_str().ok_or_else(|| anyhow!("Invalid path"))?;
diff --git a/compos/src/compsvc.rs b/compos/src/compsvc.rs
index 4330bbf..40d14d8 100644
--- a/compos/src/compsvc.rs
+++ b/compos/src/compsvc.rs
@@ -30,14 +30,16 @@
 use crate::artifact_signer::ArtifactSigner;
 use crate::compilation::odrefresh;
 use crate::compos_key;
+use authfs_aidl_interface::aidl::com::android::virt::fs::IAuthFsService::{
+    IAuthFsService, AUTHFS_SERVICE_SOCKET_NAME,
+};
 use binder::{BinderFeatures, ExceptionCode, Interface, Result as BinderResult, Status, Strong};
 use compos_aidl_interface::aidl::com::android::compos::ICompOsService::{
     BnCompOsService, ICompOsService, OdrefreshArgs::OdrefreshArgs,
 };
 use compos_common::binder::to_binder_result;
 use compos_common::odrefresh::{is_system_property_interesting, ODREFRESH_PATH};
-
-const AUTHFS_SERVICE_NAME: &str = "authfs_service";
+use rpcbinder::get_unix_domain_rpc_interface;
 
 /// Constructs a binder object that implements ICompOsService.
 pub fn new_binder() -> Result<Strong<dyn ICompOsService>> {
@@ -127,8 +129,10 @@
 
 impl CompOsService {
     fn do_odrefresh(&self, args: &OdrefreshArgs) -> Result<i8> {
-        let authfs_service = binder::get_interface(AUTHFS_SERVICE_NAME)
-            .context("Unable to connect to AuthFS service")?;
+        log::debug!("Prepare to connect to {}", AUTHFS_SERVICE_SOCKET_NAME);
+        let authfs_service: Strong<dyn IAuthFsService> =
+            get_unix_domain_rpc_interface(AUTHFS_SERVICE_SOCKET_NAME)
+                .with_context(|| format!("Failed to connect to {}", AUTHFS_SERVICE_SOCKET_NAME))?;
         let exit_code = odrefresh(&self.odrefresh_path, args, authfs_service, |output_dir| {
             // authfs only shows us the files we created, so it's ok to just sign everything
             // under the output directory.
@@ -144,7 +148,7 @@
 
 fn add_artifacts(target_dir: &Path, artifact_signer: &mut ArtifactSigner) -> Result<()> {
     for entry in
-        read_dir(&target_dir).with_context(|| format!("Traversing {}", target_dir.display()))?
+        read_dir(target_dir).with_context(|| format!("Traversing {}", target_dir.display()))?
     {
         let entry = entry?;
         let file_type = entry.file_type()?;
diff --git a/demo/Android.bp b/demo/Android.bp
index 8613166..2b234a6 100644
--- a/demo/Android.bp
+++ b/demo/Android.bp
@@ -13,7 +13,10 @@
         "com.google.android.material_material",
     ],
     libs: [
-        "android.system.virtualmachine",
+        // We need to compile against the .impl library which includes the hidden
+        // APIs. Once the APIs are promoted to @SystemApi we can switch to
+        // framework-virtualization, which contains API stubs.
+        "framework-virtualization.impl",
     ],
     jni_libs: ["MicrodroidTestNativeLib"],
     platform_apis: true,
diff --git a/demo/AndroidManifest.xml b/demo/AndroidManifest.xml
index 6669adb..17a7680 100644
--- a/demo/AndroidManifest.xml
+++ b/demo/AndroidManifest.xml
@@ -4,11 +4,11 @@
 
     <uses-permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE" />
     <uses-sdk android:minSdkVersion="33" android:targetSdkVersion="33"/>
+    <uses-feature android:name="android.software.virtualization_framework" android:required="true" />
     <application
         android:label="MicrodroidDemo"
         android:theme="@style/Theme.MicrodroidDemo"
         android:testOnly="true">
-        <uses-library android:name="android.system.virtualmachine" android:required="true" />
         <activity android:name=".MainActivity" android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
diff --git a/demo/java/com/android/microdroid/demo/MainActivity.java b/demo/java/com/android/microdroid/demo/MainActivity.java
index b5ae3d5..8e870ea 100644
--- a/demo/java/com/android/microdroid/demo/MainActivity.java
+++ b/demo/java/com/android/microdroid/demo/MainActivity.java
@@ -42,7 +42,6 @@
 import com.android.microdroid.testservice.ITestService;
 
 import java.io.BufferedReader;
-import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
@@ -169,16 +168,7 @@
                         private final ExecutorService mService = mExecutorService;
 
                         @Override
-                        public void onPayloadStarted(VirtualMachine vm,
-                                ParcelFileDescriptor stream) {
-                            if (stream == null) {
-                                mPayloadOutput.postValue("(no output available)");
-                                return;
-                            }
-
-                            InputStream input = new FileInputStream(stream.getFileDescriptor());
-                            mService.execute(new Reader("payload", mPayloadOutput, input));
-                        }
+                        public void onPayloadStarted(VirtualMachine vm) {}
 
                         @Override
                         public void onPayloadReady(VirtualMachine vm) {
diff --git a/javalib/Android.bp b/javalib/Android.bp
index cb03fa1..2982a32 100644
--- a/javalib/Android.bp
+++ b/javalib/Android.bp
@@ -11,9 +11,10 @@
 }
 
 java_sdk_library {
-    name: "android.system.virtualmachine",
-    installable: true,
-    compile_dex: true,
+    name: "framework-virtualization",
+
+    // TODO(b/243512044): introduce non-updatable-framework-module-defaults
+    defaults: ["framework-module-defaults"],
 
     jarjar_rules: "jarjar-rules.txt",
 
@@ -25,6 +26,7 @@
     ],
 
     apex_available: ["com.android.virt"],
+
     permitted_packages: [
         "android.system.virtualmachine",
         "android.system.virtualizationservice",
@@ -32,12 +34,34 @@
         "com.android.system.virtualmachine.sysprop",
     ],
     errorprone: {
-        // We use @GuardedBy and we want a test failure if our locking isn't consistent with it.
         enabled: true,
         javacflags: [
+            // We use @GuardedBy and we want a test failure if our locking isn't consistent with it.
             "-Xep:GuardedBy:ERROR",
+            // JavaApiUsedByMainlineModule is quite spammy, and since we com.android.virt is not
+            // an updatable module we don't need it.
+            "-Xep:JavaApiUsedByMainlineModule:OFF",
         ],
     },
+
+    test: {
+        enabled: true,
+        sdk_version: "module_current",
+    },
+
+    sdk_version: "core_platform",
+    stub_only_libs: [
+        "android_module_lib_stubs_current",
+    ],
+    impl_only_libs: [
+        "framework",
+    ],
+    impl_library_visibility: [
+        "//packages/modules/Virtualization:__subpackages__",
+    ],
+
+    // TODO(b/243512044): remove once we have API tracking files in prebuilts/sdk
+    unsafe_ignore_missing_latest_api: true,
 }
 
 prebuilt_apis {
diff --git a/javalib/api/module-lib-current.txt b/javalib/api/module-lib-current.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/javalib/api/module-lib-current.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/javalib/api/module-lib-removed.txt b/javalib/api/module-lib-removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/javalib/api/module-lib-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachine.java b/javalib/src/android/system/virtualmachine/VirtualMachine.java
index cf791e4..4435576 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachine.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachine.java
@@ -16,6 +16,7 @@
 
 package android.system.virtualmachine;
 
+import static android.os.ParcelFileDescriptor.AutoCloseInputStream;
 import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
 import static android.os.ParcelFileDescriptor.MODE_READ_WRITE;
 import static android.system.virtualmachine.VirtualMachineCallback.ERROR_PAYLOAD_CHANGED;
@@ -75,7 +76,7 @@
 import java.io.InputStreamReader;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.lang.ref.WeakReference;
+import java.nio.channels.FileChannel;
 import java.nio.file.FileAlreadyExistsException;
 import java.nio.file.FileVisitResult;
 import java.nio.file.Files;
@@ -84,10 +85,7 @@
 import java.nio.file.attribute.BasicFileAttributes;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
-import java.util.WeakHashMap;
 import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Consumer;
@@ -105,11 +103,6 @@
  * @hide
  */
 public class VirtualMachine implements AutoCloseable {
-    /** Map from context to a map of all that context's VMs by name. */
-    @GuardedBy("sCreateLock")
-    private static final Map<Context, Map<String, WeakReference<VirtualMachine>>> sInstances =
-            new WeakHashMap<>();
-
     /** Name of the directory under the files directory where all VMs created for the app exist. */
     private static final String VM_DIR = "vm";
 
@@ -206,17 +199,10 @@
     private static final long INSTANCE_FILE_SIZE = 10 * 1024 * 1024;
 
     // A note on lock ordering:
-    // You can take mLock while holding sCreateLock, but not vice versa.
+    // You can take mLock while holding VirtualMachineManager.sCreateLock, but not vice versa.
     // We never take any other lock while holding mCallbackLock; therefore you can
     // take mCallbackLock while holding any other lock.
 
-    /**
-     * A lock used to synchronize the creation of virtual machines. It protects
-     * {@link #sInstances}, but is also held throughout VM creation / retrieval / deletion, to
-     * prevent these actions racing with each other.
-     */
-    static final Object sCreateLock = new Object();
-
     /** Lock protecting our mutable state (other than callbacks). */
     private final Object mLock = new Object();
 
@@ -279,16 +265,44 @@
         mExtraApks = setupExtraApks(context, config, thisVmDir);
     }
 
-    @GuardedBy("sCreateLock")
+    /**
+     * Builds a virtual machine from an {@link VirtualMachineDescriptor} object and associates it
+     * with the given name.
+     *
+     * <p>The new virtual machine will be in the same state as the descriptor indicates.
+     *
+     * <p>Once a virtual machine is imported it is persisted until it is deleted by calling {@link
+     * #delete}. The imported virtual machine is in {@link #STATUS_STOPPED} state. To run the VM,
+     * call {@link #run}.
+     */
+    @GuardedBy("VirtualMachineManager.sCreateLock")
     @NonNull
-    private static Map<String, WeakReference<VirtualMachine>> getInstancesMap(Context context) {
-        return sInstances.computeIfAbsent(context, unused -> new HashMap<>());
-    }
-
-    @NonNull
-    private static File getVmDir(Context context, String name) {
-        File vmRoot = new File(context.getDataDir(), VM_DIR);
-        return new File(vmRoot, name);
+    static VirtualMachine fromDescriptor(
+            @NonNull Context context,
+            @NonNull String name,
+            @NonNull VirtualMachineDescriptor vmDescriptor)
+            throws VirtualMachineException {
+        VirtualMachineConfig config = VirtualMachineConfig.from(vmDescriptor.getConfigFd());
+        File vmDir = createVmDir(context, name);
+        try {
+            VirtualMachine vm = new VirtualMachine(context, name, config);
+            config.serialize(vm.mConfigFilePath);
+            try {
+                vm.mInstanceFilePath.createNewFile();
+            } catch (IOException e) {
+                throw new VirtualMachineException("failed to create instance image", e);
+            }
+            vm.importInstanceFrom(vmDescriptor.getInstanceImgFd());
+            return vm;
+        } catch (VirtualMachineException | RuntimeException e) {
+            // If anything goes wrong, delete any files created so far and the VM's directory
+            try {
+                deleteRecursively(vmDir);
+            } catch (IOException innerException) {
+                e.addSuppressed(innerException);
+            }
+            throw e;
+        }
     }
 
     /**
@@ -296,36 +310,16 @@
      * it is persisted until it is deleted by calling {@link #delete}. The created virtual machine
      * is in {@link #STATUS_STOPPED} state. To run the VM, call {@link #run}.
      */
-    @GuardedBy("sCreateLock")
+    @GuardedBy("VirtualMachineManager.sCreateLock")
     @NonNull
     static VirtualMachine create(
             @NonNull Context context, @NonNull String name, @NonNull VirtualMachineConfig config)
             throws VirtualMachineException {
-        File vmDir = getVmDir(context, name);
-
-        try {
-            // We don't need to undo this even if VM creation fails.
-            Files.createDirectories(vmDir.getParentFile().toPath());
-
-            // The checking of the existence of this directory and the creation of it is done
-            // atomically. If the directory already exists (i.e. the VM with the same name was
-            // already created), FileAlreadyExistsException is thrown.
-            Files.createDirectory(vmDir.toPath());
-        } catch (FileAlreadyExistsException e) {
-            throw new VirtualMachineException("virtual machine already exists", e);
-        } catch (IOException e) {
-            throw new VirtualMachineException("failed to create directory for VM", e);
-        }
+        File vmDir = createVmDir(context, name);
 
         try {
             VirtualMachine vm = new VirtualMachine(context, name, config);
-
-            try (FileOutputStream output = new FileOutputStream(vm.mConfigFilePath)) {
-                config.serialize(output);
-            } catch (IOException e) {
-                throw new VirtualMachineException("failed to write VM config", e);
-            }
-
+            config.serialize(vm.mConfigFilePath);
             try {
                 vm.mInstanceFilePath.createNewFile();
             } catch (IOException e) {
@@ -348,9 +342,6 @@
             } catch (ServiceSpecificException | IllegalArgumentException e) {
                 throw new VirtualMachineException("failed to create instance partition", e);
             }
-
-            getInstancesMap(context).put(name, new WeakReference<>(vm));
-
             return vm;
         } catch (VirtualMachineException | RuntimeException e) {
             // If anything goes wrong, delete any files created so far and the VM's directory
@@ -364,65 +355,68 @@
     }
 
     /** Loads a virtual machine that is already created before. */
-    @GuardedBy("sCreateLock")
+    @GuardedBy("VirtualMachineManager.sCreateLock")
     @Nullable
-    static VirtualMachine load(
-            @NonNull Context context, @NonNull String name) throws VirtualMachineException {
+    static VirtualMachine load(@NonNull Context context, @NonNull String name)
+            throws VirtualMachineException {
         File thisVmDir = getVmDir(context, name);
         if (!thisVmDir.exists()) {
             // The VM doesn't exist.
             return null;
         }
         File configFilePath = new File(thisVmDir, CONFIG_FILE);
-        VirtualMachineConfig config;
-        try (FileInputStream input = new FileInputStream(configFilePath)) {
-            config = VirtualMachineConfig.from(input);
-        } catch (IOException e) {
-            throw new VirtualMachineException("Failed to read config file", e);
-        }
-
-        Map<String, WeakReference<VirtualMachine>> instancesMap = getInstancesMap(context);
-
-        VirtualMachine vm = null;
-        if (instancesMap.containsKey(name)) {
-            vm = instancesMap.get(name).get();
-        }
-        if (vm == null) {
-            vm = new VirtualMachine(context, name, config);
-        }
+        VirtualMachineConfig config = VirtualMachineConfig.from(configFilePath);
+        VirtualMachine vm = new VirtualMachine(context, name, config);
 
         if (!vm.mInstanceFilePath.exists()) {
             throw new VirtualMachineException("instance image missing");
         }
 
-        instancesMap.put(name, new WeakReference<>(vm));
-
         return vm;
     }
 
-    @GuardedBy("sCreateLock")
-    static void delete(Context context, String name) throws VirtualMachineException {
-        Map<String, WeakReference<VirtualMachine>> instancesMap = sInstances.get(context);
-        VirtualMachine vm;
-        if (instancesMap != null && instancesMap.containsKey(name)) {
-            vm = instancesMap.get(name).get();
-        } else {
-            vm = null;
+    @GuardedBy("VirtualMachineManager.sCreateLock")
+    void delete(Context context, String name) throws VirtualMachineException {
+        synchronized (mLock) {
+            checkStopped();
         }
 
-        if (vm != null) {
-            synchronized (vm.mLock) {
-                vm.checkStopped();
-            }
-        }
+        deleteVmDirectory(context, name);
+    }
 
+    static void deleteVmDirectory(Context context, String name) throws VirtualMachineException {
         try {
             deleteRecursively(getVmDir(context, name));
         } catch (IOException e) {
             throw new VirtualMachineException(e);
         }
+    }
 
-        if (instancesMap != null) instancesMap.remove(name);
+    @GuardedBy("VirtualMachineManager.sCreateLock")
+    @NonNull
+    private static File createVmDir(@NonNull Context context, @NonNull String name)
+            throws VirtualMachineException {
+        File vmDir = getVmDir(context, name);
+        try {
+            // We don't need to undo this even if VM creation fails.
+            Files.createDirectories(vmDir.getParentFile().toPath());
+
+            // The checking of the existence of this directory and the creation of it is done
+            // atomically. If the directory already exists (i.e. the VM with the same name was
+            // already created), FileAlreadyExistsException is thrown.
+            Files.createDirectory(vmDir.toPath());
+        } catch (FileAlreadyExistsException e) {
+            throw new VirtualMachineException("virtual machine already exists", e);
+        } catch (IOException e) {
+            throw new VirtualMachineException("failed to create directory for VM", e);
+        }
+        return vmDir;
+    }
+
+    @NonNull
+    private static File getVmDir(Context context, String name) {
+        File vmRoot = new File(context.getDataDir(), VM_DIR);
+        return new File(vmRoot, name);
     }
 
     /**
@@ -643,9 +637,8 @@
                 mVirtualMachine.registerCallback(
                         new IVirtualMachineCallback.Stub() {
                             @Override
-                            public void onPayloadStarted(int cid, ParcelFileDescriptor stream) {
-                                executeCallback(
-                                        (cb) -> cb.onPayloadStarted(VirtualMachine.this, stream));
+                            public void onPayloadStarted(int cid) {
+                                executeCallback((cb) -> cb.onPayloadStarted(VirtualMachine.this));
                             }
 
                             @Override
@@ -656,16 +649,20 @@
                             @Override
                             public void onPayloadFinished(int cid, int exitCode) {
                                 executeCallback(
-                                        (cb) -> cb.onPayloadFinished(VirtualMachine.this,
-                                                exitCode));
+                                        (cb) ->
+                                                cb.onPayloadFinished(
+                                                        VirtualMachine.this, exitCode));
                             }
 
                             @Override
                             public void onError(int cid, int errorCode, String message) {
                                 int translatedError = getTranslatedError(errorCode);
                                 executeCallback(
-                                        (cb) -> cb.onError(VirtualMachine.this, translatedError,
-                                                message));
+                                        (cb) ->
+                                                cb.onError(
+                                                        VirtualMachine.this,
+                                                        translatedError,
+                                                        message));
                             }
 
                             @Override
@@ -674,18 +671,17 @@
                                 int translatedReason = getTranslatedReason(reason);
                                 if (onDiedCalled.compareAndSet(false, true)) {
                                     executeCallback(
-                                            (cb) -> cb.onStopped(VirtualMachine.this,
-                                                    translatedReason));
+                                            (cb) ->
+                                                    cb.onStopped(
+                                                            VirtualMachine.this, translatedReason));
                                 }
                             }
 
                             @Override
                             public void onRamdump(int cid, ParcelFileDescriptor ramdump) {
-                                executeCallback(
-                                        (cb) -> cb.onRamdump(VirtualMachine.this, ramdump));
+                                executeCallback((cb) -> cb.onRamdump(VirtualMachine.this, ramdump));
                             }
-                        }
-                );
+                        });
                 service.asBinder().linkToDeath(deathRecipient, 0);
                 mVirtualMachine.start();
             } catch (IOException | IllegalStateException | ServiceSpecificException e) {
@@ -838,14 +834,7 @@
                 throw new VirtualMachineException("incompatible config");
             }
             checkStopped();
-
-            try {
-                FileOutputStream output = new FileOutputStream(mConfigFilePath);
-                newConfig.serialize(output);
-                output.close();
-            } catch (IOException e) {
-                throw new VirtualMachineException("Failed to persist config", e);
-            }
+            newConfig.serialize(mConfigFilePath);
             mConfig = newConfig;
             return oldConfig;
         }
@@ -901,6 +890,7 @@
      * @return a {@link VirtualMachineDescriptor} instance that represents the VM's state.
      * @throws VirtualMachineException if the virtual machine is not stopped, or the state could not
      *     be captured.
+     * @hide
      */
     @NonNull
     public VirtualMachineDescriptor toDescriptor() throws VirtualMachineException {
@@ -1065,4 +1055,14 @@
             throw new VirtualMachineException("Couldn't parse extra apks from the vm config", e);
         }
     }
+
+    private void importInstanceFrom(@NonNull ParcelFileDescriptor instanceFd)
+            throws VirtualMachineException {
+        try (FileChannel instance = new FileOutputStream(mInstanceFilePath).getChannel();
+                FileChannel instanceInput = new AutoCloseInputStream(instanceFd).getChannel()) {
+            instance.transferFrom(instanceInput, /*position=*/ 0, instanceInput.size());
+        } catch (IOException e) {
+            throw new VirtualMachineException("failed to transfer instance image", e);
+        }
+    }
 }
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java b/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java
index bb6b2b8..1f94a8b 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java
@@ -18,7 +18,6 @@
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.os.ParcelFileDescriptor;
 
@@ -135,11 +134,8 @@
     /** The VM killed due to hangup */
     int STOP_REASON_HANGUP = 16;
 
-    /**
-     * Called when the payload starts in the VM. The stream, if non-null, provides access
-     * to the stdin/stdout of the VM payload.
-     */
-    void onPayloadStarted(@NonNull VirtualMachine vm, @Nullable ParcelFileDescriptor stream);
+    /** Called when the payload starts in the VM. */
+    void onPayloadStarted(@NonNull VirtualMachine vm);
 
     /**
      * Called when the payload in the VM is ready to serve. See
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
index b814367..a660306 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -16,6 +16,7 @@
 
 package android.system.virtualmachine;
 
+import static android.os.ParcelFileDescriptor.AutoCloseInputStream;
 import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
 
 import static java.util.Objects.requireNonNull;
@@ -33,7 +34,9 @@
 import android.system.virtualizationservice.VirtualMachinePayloadConfig;
 
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -181,9 +184,30 @@
         mNumCpus = numCpus;
     }
 
+    /** Loads a config from a file. */
+    @NonNull
+    static VirtualMachineConfig from(@NonNull File file) throws VirtualMachineException {
+        try (FileInputStream input = new FileInputStream(file)) {
+            return fromInputStream(input);
+        } catch (IOException e) {
+            throw new VirtualMachineException("Failed to read VM config from file", e);
+        }
+    }
+
+    /** Loads a config from a {@link ParcelFileDescriptor}. */
+    @NonNull
+    static VirtualMachineConfig from(@NonNull ParcelFileDescriptor fd)
+            throws VirtualMachineException {
+        try (AutoCloseInputStream input = new AutoCloseInputStream(fd)) {
+            return fromInputStream(input);
+        } catch (IOException e) {
+            throw new VirtualMachineException("failed to read VM config from file descriptor", e);
+        }
+    }
+
     /** Loads a config from a stream, for example a file. */
     @NonNull
-    static VirtualMachineConfig from(@NonNull InputStream input)
+    private static VirtualMachineConfig fromInputStream(@NonNull InputStream input)
             throws IOException, VirtualMachineException {
         PersistableBundle b = PersistableBundle.readFromStream(input);
         int version = b.getInt(KEY_VERSION);
@@ -215,8 +239,17 @@
                 protectedVm, memoryMib, numCpus);
     }
 
+    /** Persists this config to a file. */
+    void serialize(@NonNull File file) throws VirtualMachineException {
+        try (FileOutputStream output = new FileOutputStream(file)) {
+            serializeOutputStream(output);
+        } catch (IOException e) {
+            throw new VirtualMachineException("failed to write VM config", e);
+        }
+    }
+
     /** Persists this config to a stream, for example a file. */
-    void serialize(@NonNull OutputStream output) throws IOException {
+    private void serializeOutputStream(@NonNull OutputStream output) throws IOException {
         PersistableBundle b = new PersistableBundle();
         b.putInt(KEY_VERSION, VERSION);
         b.putString(KEY_APKPATH, mApkPath);
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineDescriptor.java b/javalib/src/android/system/virtualmachine/VirtualMachineDescriptor.java
index 70532fc..b51cbce 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineDescriptor.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineDescriptor.java
@@ -23,8 +23,6 @@
 import android.os.ParcelFileDescriptor;
 import android.os.Parcelable;
 
-import com.android.internal.annotations.VisibleForTesting;
-
 /**
  * A VM descriptor that captures the state of a Virtual Machine.
  *
@@ -35,8 +33,8 @@
  * @hide
  */
 public final class VirtualMachineDescriptor implements Parcelable {
-    private final @NonNull ParcelFileDescriptor mConfigFd;
-    private final @NonNull ParcelFileDescriptor mInstanceImgFd;
+    @NonNull private final ParcelFileDescriptor mConfigFd;
+    @NonNull private final ParcelFileDescriptor mInstanceImgFd;
     // TODO(b/243129654): Add trusted storage fd once it is available.
 
     @Override
@@ -45,13 +43,14 @@
     }
 
     @Override
-    public void writeToParcel(Parcel out, int flags) {
+    public void writeToParcel(@NonNull Parcel out, int flags) {
         mConfigFd.writeToParcel(out, flags);
         mInstanceImgFd.writeToParcel(out, flags);
     }
 
+    @NonNull
     public static final Parcelable.Creator<VirtualMachineDescriptor> CREATOR =
-            new Parcelable.Creator<VirtualMachineDescriptor>() {
+            new Parcelable.Creator<>() {
                 public VirtualMachineDescriptor createFromParcel(Parcel in) {
                     return new VirtualMachineDescriptor(in);
                 }
@@ -63,19 +62,17 @@
 
     /**
      * @return File descriptor of the VM configuration file config.xml.
-     * @hide
      */
-    @VisibleForTesting
-    public @NonNull ParcelFileDescriptor getConfigFd() {
+    @NonNull
+    ParcelFileDescriptor getConfigFd() {
         return mConfigFd;
     }
 
     /**
      * @return File descriptor of the instance.img of the VM.
-     * @hide
      */
-    @VisibleForTesting
-    public @NonNull ParcelFileDescriptor getInstanceImgFd() {
+    @NonNull
+    ParcelFileDescriptor getInstanceImgFd() {
         return mInstanceImgFd;
     }
 
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineManager.java b/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
index 34b9fd9..0e96f43 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
@@ -25,6 +25,7 @@
 import android.annotation.SuppressLint;
 import android.content.Context;
 import android.sysprop.HypervisorProperties;
+import android.util.ArrayMap;
 
 import com.android.internal.annotations.GuardedBy;
 
@@ -47,6 +48,13 @@
  * @hide
  */
 public class VirtualMachineManager {
+    /**
+     * A lock used to synchronize the creation of virtual machines. It protects {@link #mVmsByName},
+     * but is also held throughout VM creation / retrieval / deletion, to prevent these actions
+     * racing with each other.
+     */
+    private static final Object sCreateLock = new Object();
+
     @NonNull private final Context mContext;
 
     private VirtualMachineManager(@NonNull Context context) {
@@ -57,6 +65,9 @@
     private static final Map<Context, WeakReference<VirtualMachineManager>> sInstances =
             new WeakHashMap<>();
 
+    @GuardedBy("sCreateLock")
+    private final Map<String, WeakReference<VirtualMachine>> mVmsByName = new ArrayMap<>();
+
     /**
      * Capabilities of the virtual machine implementation.
      *
@@ -136,23 +147,65 @@
     public VirtualMachine create(
             @NonNull String name, @NonNull VirtualMachineConfig config)
             throws VirtualMachineException {
-        synchronized (VirtualMachine.sCreateLock) {
-            return VirtualMachine.create(mContext, name, config);
+        synchronized (sCreateLock) {
+            return createLocked(name, config);
         }
     }
 
+    @NonNull
+    @GuardedBy("sCreateLock")
+    private VirtualMachine createLocked(@NonNull String name, @NonNull VirtualMachineConfig config)
+            throws VirtualMachineException {
+        VirtualMachine vm = VirtualMachine.create(mContext, name, config);
+        mVmsByName.put(name, new WeakReference<>(vm));
+        return vm;
+    }
+
     /**
      * Returns an existing {@link VirtualMachine} with the given name. Returns null if there is no
      * such virtual machine.
      *
      * @throws VirtualMachineException if the virtual machine exists but could not be successfully
-     *                                 retrieved.
+     *     retrieved.
      * @hide
      */
     @Nullable
     public VirtualMachine get(@NonNull String name) throws VirtualMachineException {
-        synchronized (VirtualMachine.sCreateLock) {
-            return VirtualMachine.load(mContext, name);
+        synchronized (sCreateLock) {
+            return getLocked(name);
+        }
+    }
+
+    @Nullable
+    @GuardedBy("sCreateLock")
+    private VirtualMachine getLocked(@NonNull String name) throws VirtualMachineException {
+        VirtualMachine vm = getVmByName(name);
+        if (vm != null) return vm;
+
+        vm = VirtualMachine.load(mContext, name);
+        if (vm != null) {
+            mVmsByName.put(name, new WeakReference<>(vm));
+        }
+        return vm;
+    }
+
+    /**
+     * Imports a virtual machine from an {@link VirtualMachineDescriptor} object and associates it
+     * with the given name.
+     *
+     * <p>The new virtual machine will be in the same state as the descriptor indicates.
+     *
+     * @throws VirtualMachineException if the VM cannot be imported.
+     * @hide
+     */
+    @NonNull
+    public VirtualMachine importFromDescriptor(
+            @NonNull String name, @NonNull VirtualMachineDescriptor vmDescriptor)
+            throws VirtualMachineException {
+        synchronized (sCreateLock) {
+            VirtualMachine vm = VirtualMachine.fromDescriptor(mContext, name, vmDescriptor);
+            mVmsByName.put(name, new WeakReference<>(vm));
+            return vm;
         }
     }
 
@@ -167,14 +220,14 @@
     public VirtualMachine getOrCreate(
             @NonNull String name, @NonNull VirtualMachineConfig config)
             throws VirtualMachineException {
-        VirtualMachine vm;
-        synchronized (VirtualMachine.sCreateLock) {
-            vm = get(name);
-            if (vm == null) {
-                vm = create(name, config);
+        synchronized (sCreateLock) {
+            VirtualMachine vm = getLocked(name);
+            if (vm != null) {
+                return vm;
+            } else {
+                return createLocked(name, config);
             }
         }
-        return vm;
     }
 
     /**
@@ -189,9 +242,28 @@
      * @hide
      */
     public void delete(@NonNull String name) throws VirtualMachineException {
-        requireNonNull(name);
-        synchronized (VirtualMachine.sCreateLock) {
-            VirtualMachine.delete(mContext, name);
+        synchronized (sCreateLock) {
+            VirtualMachine vm = getVmByName(name);
+            if (vm == null) {
+                VirtualMachine.deleteVmDirectory(mContext, name);
+            } else {
+                vm.delete(mContext, name);
+            }
+            mVmsByName.remove(name);
         }
     }
+
+    @Nullable
+    @GuardedBy("sCreateLock")
+    private VirtualMachine getVmByName(@NonNull String name) {
+        requireNonNull(name);
+        WeakReference<VirtualMachine> weakReference = mVmsByName.get(name);
+        if (weakReference != null) {
+            VirtualMachine vm = weakReference.get();
+            if (vm != null && vm.getStatus() != VirtualMachine.STATUS_DELETED) {
+                return vm;
+            }
+        }
+        return null;
+    }
 }
diff --git a/libs/devicemapper/src/lib.rs b/libs/devicemapper/src/lib.rs
index b9fb5c3..ebe71e4 100644
--- a/libs/devicemapper/src/lib.rs
+++ b/libs/devicemapper/src/lib.rs
@@ -212,7 +212,7 @@
         dm_dev_suspend(self, &mut data).context("failed to activate")?;
 
         // Step 4: wait unti the device is created and return the device path
-        let path = Path::new(MAPPER_DEV_ROOT).join(&name);
+        let path = Path::new(MAPPER_DEV_ROOT).join(name);
         wait_for_path(&path)?;
         Ok(path)
     }
@@ -250,13 +250,13 @@
     }
 
     fn write_to_dev(path: &Path, data: &[u8]) {
-        let mut f = OpenOptions::new().read(true).write(true).open(&path).unwrap();
+        let mut f = OpenOptions::new().read(true).write(true).open(path).unwrap();
         f.write_all(data).unwrap();
     }
 
     fn delete_device(dm: &DeviceMapper, name: &str) -> Result<()> {
         dm.delete_device_deferred(name)?;
-        wait_for_path_disappears(Path::new(MAPPER_DEV_ROOT).join(&name))?;
+        wait_for_path_disappears(Path::new(MAPPER_DEV_ROOT).join(name))?;
         Ok(())
     }
 
diff --git a/microdroid/Android.bp b/microdroid/Android.bp
index 68827df..d73a03c 100644
--- a/microdroid/Android.bp
+++ b/microdroid/Android.bp
@@ -72,7 +72,6 @@
         "debuggerd",
         "linker",
         "linkerconfig",
-        "servicemanager.microdroid",
         "tombstoned.microdroid",
         "tombstone_transmit.microdroid",
         "cgroups.json",
diff --git a/microdroid/init.rc b/microdroid/init.rc
index 9c62782..94ef940 100644
--- a/microdroid/init.rc
+++ b/microdroid/init.rc
@@ -26,20 +26,6 @@
     setprop ro.log.file_logger.path /dev/null
 
 on init
-    # Mount binderfs
-    mkdir /dev/binderfs
-    mount binder binder /dev/binderfs stats=global
-    chmod 0755 /dev/binderfs
-
-    symlink /dev/binderfs/binder /dev/binder
-    symlink /dev/binderfs/vndbinder /dev/vndbinder
-
-    chmod 0666 /dev/binderfs/binder
-    chmod 0666 /dev/binderfs/vndbinder
-
-    start servicemanager
-
-on init
     mkdir /mnt/apk 0755 system system
     mkdir /mnt/extra-apk 0755 root root
     # Microdroid_manager starts apkdmverity/zipfuse/apexd
diff --git a/microdroid/vm_payload/Android.bp b/microdroid/vm_payload/Android.bp
index e153f92..dd2a937 100644
--- a/microdroid/vm_payload/Android.bp
+++ b/microdroid/vm_payload/Android.bp
@@ -14,6 +14,7 @@
         "libanyhow",
         "libbinder_rs",
         "liblazy_static",
+        "liblibc",
         "liblog_rust",
         "librpcbinder_rs",
     ],
diff --git a/microdroid/vm_payload/include/vm_payload.h b/microdroid/vm_payload/include/vm_payload.h
index 82dbd6d..48518ff 100644
--- a/microdroid/vm_payload/include/vm_payload.h
+++ b/microdroid/vm_payload/include/vm_payload.h
@@ -56,8 +56,9 @@
                                   void (*on_ready)(void *param), void *param);
 
 /**
- * Get a secret that is uniquely bound to this VM instance. The secrets are 32-byte values and the
- * value associated with an identifier will not change over the lifetime of the VM instance.
+ * Get a secret that is uniquely bound to this VM instance. The secrets are
+ * values up to 32 bytes long and the value associated with an identifier will
+ * not change over the lifetime of the VM instance.
  *
  * \param identifier identifier of the secret to return.
  * \param identifier_size size of the secret identifier.
@@ -80,4 +81,17 @@
  */
 const char *AVmPayload_getApkContentsPath(void);
 
+/**
+ * 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.
+ *
+ * \return the path to the APK contents, or NULL if no encrypted storage was
+ * requested in the VM configuration. If non-null the returned string should not
+ * be deleted or freed by the application and remains valid for the lifetime of
+ * the VM.
+ */
+const char *AVmPayload_getEncryptedStoragePath(void);
+
 __END_DECLS
diff --git a/microdroid/vm_payload/src/vm_payload_service.rs b/microdroid/vm_payload/src/vm_payload_service.rs
index 098d246..88484cc 100644
--- a/microdroid/vm_payload/src/vm_payload_service.rs
+++ b/microdroid/vm_payload/src/vm_payload_service.rs
@@ -23,19 +23,44 @@
 use rpcbinder::{get_unix_domain_rpc_interface, run_vsock_rpc_server};
 use std::ffi::CString;
 use std::os::raw::{c_char, c_void};
+use std::ptr;
+use std::sync::Mutex;
 
 lazy_static! {
     static ref VM_APK_CONTENTS_PATH_C: CString =
         CString::new(VM_APK_CONTENTS_PATH).expect("CString::new failed");
+    static ref PAYLOAD_CONNECTION: Mutex<Option<Strong<dyn IVmPayloadService>>> = Mutex::default();
+}
+
+/// Return a connection to the payload service in Microdroid Manager. Uses the existing connection
+/// if there is one, otherwise attempts to create a new one.
+fn get_vm_payload_service() -> Result<Strong<dyn IVmPayloadService>> {
+    let mut connection = PAYLOAD_CONNECTION.lock().unwrap();
+    if let Some(strong) = &*connection {
+        Ok(strong.clone())
+    } else {
+        let new_connection: Strong<dyn IVmPayloadService> = get_unix_domain_rpc_interface(
+            VM_PAYLOAD_SERVICE_SOCKET_NAME,
+        )
+        .context(format!("Failed to connect to service: {}", VM_PAYLOAD_SERVICE_SOCKET_NAME))?;
+        *connection = Some(new_connection.clone());
+        Ok(new_connection)
+    }
+}
+
+/// Make sure our logging goes to logcat. It is harmless to call this more than once.
+fn initialize_logging() {
+    android_logger::init_once(
+        android_logger::Config::default().with_tag("vm_payload").with_min_level(Level::Debug),
+    );
 }
 
 /// Notifies the host that the payload is ready.
 /// Returns true if the notification succeeds else false.
 #[no_mangle]
 pub extern "C" fn AVmPayload_notifyPayloadReady() -> bool {
-    android_logger::init_once(
-        android_logger::Config::default().with_tag("vm_payload").with_min_level(Level::Debug),
-    );
+    initialize_logging();
+
     if let Err(e) = try_notify_payload_ready() {
         error!("{:?}", e);
         false
@@ -74,6 +99,8 @@
     on_ready: Option<unsafe extern "C" fn(param: *mut c_void)>,
     param: *mut c_void,
 ) -> bool {
+    initialize_logging();
+
     // SAFETY: AIBinder returned has correct reference count, and the ownership can
     // safely be taken by new_spibinder.
     let service = new_spibinder(service);
@@ -106,6 +133,8 @@
     secret: *mut u8,
     size: usize,
 ) -> bool {
+    initialize_logging();
+
     let identifier = std::slice::from_raw_parts(identifier, identifier_size);
     match try_get_vm_instance_secret(identifier, size) {
         Err(e) => {
@@ -145,6 +174,8 @@
     size: usize,
     total: *mut usize,
 ) -> bool {
+    initialize_logging();
+
     match try_get_dice_attestation_chain() {
         Err(e) => {
             error!("{:?}", e);
@@ -179,6 +210,8 @@
     size: usize,
     total: *mut usize,
 ) -> bool {
+    initialize_logging();
+
     match try_get_dice_attestation_cdi() {
         Err(e) => {
             error!("{:?}", e);
@@ -198,11 +231,13 @@
     (*VM_APK_CONTENTS_PATH_C).as_ptr()
 }
 
-fn try_get_dice_attestation_cdi() -> Result<Vec<u8>> {
-    get_vm_payload_service()?.getDiceAttestationCdi().context("Cannot get attestation CDI")
+/// Gets the path to the VM's encrypted storage.
+#[no_mangle]
+pub extern "C" fn AVmPayload_getEncryptedStoragePath() -> *const c_char {
+    // TODO(b/254454578): Return a real path if storage is present
+    ptr::null()
 }
 
-fn get_vm_payload_service() -> Result<Strong<dyn IVmPayloadService>> {
-    get_unix_domain_rpc_interface(VM_PAYLOAD_SERVICE_SOCKET_NAME)
-        .context(format!("Failed to connect to service: {}", VM_PAYLOAD_SERVICE_SOCKET_NAME))
+fn try_get_dice_attestation_cdi() -> Result<Vec<u8>> {
+    get_vm_payload_service()?.getDiceAttestationCdi().context("Cannot get attestation CDI")
 }
diff --git a/microdroid_manager/src/main.rs b/microdroid_manager/src/main.rs
index cc4b9dc..4ebcc2e 100644
--- a/microdroid_manager/src/main.rs
+++ b/microdroid_manager/src/main.rs
@@ -26,9 +26,12 @@
 use crate::vm_payload_service::register_vm_payload_service;
 use android_system_virtualizationcommon::aidl::android::system::virtualizationcommon::ErrorCode::ErrorCode;
 use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice::IVirtualMachineService::{
-        IVirtualMachineService, VM_BINDER_SERVICE_PORT, VM_STREAM_SERVICE_PORT,
+        IVirtualMachineService, VM_BINDER_SERVICE_PORT,
 };
-use android_system_virtualization_payload::aidl::android::system::virtualization::payload::IVmPayloadService::VM_APK_CONTENTS_PATH;
+use android_system_virtualization_payload::aidl::android::system::virtualization::payload::IVmPayloadService::{
+    VM_APK_CONTENTS_PATH,
+    VM_PAYLOAD_SERVICE_SOCKET_NAME,
+};
 use anyhow::{anyhow, bail, ensure, Context, Error, Result};
 use apkverify::{get_public_key_der, verify, V4Signature};
 use binder::Strong;
@@ -36,28 +39,28 @@
 use glob::glob;
 use itertools::sorted;
 use libc::VMADDR_CID_HOST;
-use log::{error, info};
+use log::{error, info, warn};
 use microdroid_metadata::{write_metadata, Metadata, PayloadMetadata};
 use microdroid_payload_config::{OsConfig, Task, TaskType, VmPayloadConfig};
+use nix::fcntl::{fcntl, F_SETFD, FdFlag};
 use nix::sys::signal::Signal;
 use openssl::sha::Sha512;
 use payload::{get_apex_data_from_payload, load_metadata, to_metadata};
 use rand::Fill;
 use rpcbinder::get_vsock_rpc_interface;
+use rustutils::sockets::android_get_control_socket;
 use rustutils::system_properties;
 use rustutils::system_properties::PropertyWatcher;
 use std::borrow::Cow::{Borrowed, Owned};
 use std::convert::TryInto;
 use std::env;
-use std::fs::{self, create_dir, File, OpenOptions};
+use std::fs::{self, create_dir, OpenOptions};
 use std::io::Write;
-use std::os::unix::io::{FromRawFd, IntoRawFd};
 use std::os::unix::process::ExitStatusExt;
 use std::path::Path;
 use std::process::{Child, Command, Stdio};
 use std::str;
 use std::time::{Duration, SystemTime};
-use vsock::VsockStream;
 
 const WAIT_TIMEOUT: Duration = Duration::from_secs(10);
 const MAIN_APK_PATH: &str = "/dev/block/by-name/microdroid-apk";
@@ -182,10 +185,22 @@
     })
 }
 
+fn set_cloexec_on_vm_payload_service_socket() -> Result<()> {
+    let fd = android_get_control_socket(VM_PAYLOAD_SERVICE_SOCKET_NAME)?;
+
+    fcntl(fd, F_SETFD(FdFlag::FD_CLOEXEC))?;
+
+    Ok(())
+}
+
 fn try_main() -> Result<()> {
     let _ = kernlog::init();
     info!("started.");
 
+    if let Err(e) = set_cloexec_on_vm_payload_service_socket() {
+        warn!("Failed to set cloexec on vm payload socket: {:?}", e);
+    }
+
     load_crashkernel_if_supported().context("Failed to load crashkernel")?;
 
     swap::init_swap().context("Failed to initialise swap")?;
@@ -444,8 +459,6 @@
 fn run_apkdmverity(args: &[ApkDmverityArgument]) -> Result<Child> {
     let mut cmd = Command::new(APKDMVERITY_BIN);
 
-    cmd.stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null());
-
     for argument in args {
         cmd.arg("--apk").arg(argument.apk).arg(argument.idsig).arg(argument.name);
         if let Some(root_hash) = argument.saved_root_hash {
@@ -477,15 +490,7 @@
     if let Some(property_name) = ready_prop {
         cmd.args(["-p", property_name]);
     }
-    cmd.arg("-o")
-        .arg(option)
-        .arg(zip_path)
-        .arg(mount_dir)
-        .stdin(Stdio::null())
-        .stdout(Stdio::null())
-        .stderr(Stdio::null())
-        .spawn()
-        .context("Spawn zipfuse")
+    cmd.arg("-o").arg(option).arg(zip_path).arg(mount_dir).spawn().context("Spawn zipfuse")
 }
 
 fn write_apex_payload_data(
@@ -747,11 +752,18 @@
     Ok(())
 }
 
-/// Executes the given task. Stdout of the task is piped into the vsock stream to the
-/// virtualizationservice in the host side.
+/// Executes the given task.
 fn exec_task(task: &Task, service: &Strong<dyn IVirtualMachineService>) -> Result<i32> {
     info!("executing main task {:?}...", task);
-    let mut command = build_command(task)?;
+    let mut command = match task.type_ {
+        TaskType::Executable => Command::new(&task.command),
+        TaskType::MicrodroidLauncher => {
+            let mut command = Command::new("/system/bin/microdroid_launcher");
+            command.arg(find_library_path(&task.command)?);
+            command
+        }
+    };
+    command.stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null());
 
     info!("notifying payload started");
     service.notifyPayloadStarted()?;
@@ -770,40 +782,6 @@
     }
 }
 
-fn build_command(task: &Task) -> Result<Command> {
-    let mut command = match task.type_ {
-        TaskType::Executable => Command::new(&task.command),
-        TaskType::MicrodroidLauncher => {
-            let mut command = Command::new("/system/bin/microdroid_launcher");
-            command.arg(find_library_path(&task.command)?);
-            command
-        }
-    };
-
-    match VsockStream::connect_with_cid_port(VMADDR_CID_HOST, VM_STREAM_SERVICE_PORT as u32) {
-        Ok(stream) => {
-            // SAFETY: the ownership of the underlying file descriptor is transferred from stream
-            // to the file object, and then into the Command object. When the command is finished,
-            // the file descriptor is closed.
-            let file = unsafe { File::from_raw_fd(stream.into_raw_fd()) };
-            command
-                .stdin(Stdio::from(file.try_clone()?))
-                .stdout(Stdio::from(file.try_clone()?))
-                .stderr(Stdio::from(file));
-        }
-        Err(e) => {
-            error!("failed to connect to virtualization service: {}", e);
-            // Don't fail hard here. Even if we failed to connect to the virtualizationservice,
-            // we keep executing the task. This can happen if the owner of the VM doesn't register
-            // callback to accept the stream. Use /dev/null as the stream so that the task can
-            // make progress without waiting for someone to consume the output.
-            command.stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null());
-        }
-    }
-
-    Ok(command)
-}
-
 fn find_library_path(name: &str) -> Result<String> {
     let mut watcher = PropertyWatcher::new("ro.product.cpu.abilist")?;
     let value = watcher.read(|_name, value| Ok(value.trim().to_string()))?;
diff --git a/pvmfw/Android.bp b/pvmfw/Android.bp
index 71bac72..77de696 100644
--- a/pvmfw/Android.bp
+++ b/pvmfw/Android.bp
@@ -8,6 +8,9 @@
     defaults: ["vmbase_ffi_defaults"],
     srcs: ["src/main.rs"],
     edition: "2021",
+    features: [
+        "legacy",
+    ],
     rustlibs: [
         "libbuddy_system_allocator",
         "liblog_rust_nostd",
diff --git a/pvmfw/src/entry.rs b/pvmfw/src/entry.rs
index dc2087d..c0ad878 100644
--- a/pvmfw/src/entry.rs
+++ b/pvmfw/src/entry.rs
@@ -19,11 +19,15 @@
 use crate::mmio_guard;
 use core::arch::asm;
 use core::slice;
-use log::{debug, error, LevelFilter};
-use vmbase::{console, logger, main, power::reboot};
+use log::debug;
+use log::error;
+use log::LevelFilter;
+use vmbase::{console, layout, logger, main, power::reboot};
 
 #[derive(Debug, Clone)]
 enum RebootReason {
+    /// A malformed BCC was received.
+    InvalidBcc,
     /// An unexpected internal error happened.
     InternalError,
 }
@@ -80,8 +84,17 @@
         RebootReason::InternalError
     })?;
 
+    // SAFETY - We only get the appended payload from here, once. It is mapped and the linker
+    // script prevents it from overlapping with other objects.
+    let bcc = as_bcc(unsafe { get_appended_data_slice() }).ok_or_else(|| {
+        error!("Invalid BCC");
+        RebootReason::InvalidBcc
+    })?;
+
     // This wrapper allows main() to be blissfully ignorant of platform details.
-    crate::main(fdt, payload);
+    crate::main(fdt, payload, bcc);
+
+    // TODO: Overwrite BCC before jumping to payload to avoid leaking our sealing key.
 
     mmio_guard::unmap(console::BASE_ADDRESS).map_err(|e| {
         error!("Failed to unshare the UART: {e}");
@@ -147,3 +160,22 @@
         );
     };
 }
+
+unsafe fn get_appended_data_slice() -> &'static mut [u8] {
+    let base = helpers::align_up(layout::binary_end(), helpers::SIZE_4KB).unwrap();
+    // pvmfw is contained in a 2MiB region so the payload can't be larger than the 2MiB alignment.
+    let size = helpers::align_up(base, helpers::SIZE_2MB).unwrap() - base;
+
+    slice::from_raw_parts_mut(base as *mut u8, size)
+}
+
+fn as_bcc(data: &mut [u8]) -> Option<&mut [u8]> {
+    const BCC_SIZE: usize = helpers::SIZE_4KB;
+
+    if cfg!(feature = "legacy") {
+        // TODO(b/256148034): return None if BccHandoverParse(bcc) != kDiceResultOk.
+        Some(&mut data[..BCC_SIZE])
+    } else {
+        None
+    }
+}
diff --git a/pvmfw/src/helpers.rs b/pvmfw/src/helpers.rs
index adfc189..59cf9f3 100644
--- a/pvmfw/src/helpers.rs
+++ b/pvmfw/src/helpers.rs
@@ -17,12 +17,25 @@
 pub const SIZE_4KB: usize = 4 << 10;
 pub const SIZE_2MB: usize = 2 << 20;
 
-/// Computes the address of the page containing a given address.
-pub const fn page_of(addr: usize, page_size: usize) -> usize {
-    addr & !(page_size - 1)
+/// Computes the largest multiple of the provided alignment smaller or equal to the address.
+///
+/// Note: the result is undefined if alignment isn't a power of two.
+pub const fn unchecked_align_down(addr: usize, alignment: usize) -> usize {
+    addr & !(alignment - 1)
+}
+
+/// Safe wrapper around unchecked_align_up() that validates its assumptions and doesn't wrap.
+pub const fn align_up(addr: usize, alignment: usize) -> Option<usize> {
+    if !alignment.is_power_of_two() {
+        None
+    } else if let Some(s) = addr.checked_add(alignment - 1) {
+        Some(unchecked_align_down(s, alignment))
+    } else {
+        None
+    }
 }
 
 /// Computes the address of the 4KiB page containing a given address.
 pub const fn page_4kb_of(addr: usize) -> usize {
-    page_of(addr, SIZE_4KB)
+    unchecked_align_down(addr, SIZE_4KB)
 }
diff --git a/pvmfw/src/main.rs b/pvmfw/src/main.rs
index c0bb263..8178d0b 100644
--- a/pvmfw/src/main.rs
+++ b/pvmfw/src/main.rs
@@ -29,7 +29,7 @@
 use avb::PUBLIC_KEY;
 use log::{debug, info};
 
-fn main(fdt: &mut [u8], payload: &[u8]) {
+fn main(fdt: &mut [u8], payload: &[u8], bcc: &[u8]) {
     info!("pVM firmware");
     debug!(
         "fdt_address={:#018x}, payload_start={:#018x}, payload_size={:#018x}",
@@ -37,6 +37,7 @@
         payload.as_ptr() as usize,
         payload.len(),
     );
+    debug!("BCC: {:?} ({:#x} bytes)", bcc.as_ptr(), bcc.len());
     debug!("AVB public key: addr={:?}, size={:#x} ({1})", PUBLIC_KEY.as_ptr(), PUBLIC_KEY.len());
     info!("Starting payload...");
 }
diff --git a/tests/aidl/com/android/microdroid/testservice/IBenchmarkService.aidl b/tests/aidl/com/android/microdroid/testservice/IBenchmarkService.aidl
index 260f804..16e4893 100644
--- a/tests/aidl/com/android/microdroid/testservice/IBenchmarkService.aidl
+++ b/tests/aidl/com/android/microdroid/testservice/IBenchmarkService.aidl
@@ -25,7 +25,7 @@
      *
      * @return The read rate in MB/s.
      */
-    double measureReadRate(String filename, long fileSizeBytes, boolean isRand);
+    double measureReadRate(String filename, boolean isRand);
 
     /** Returns an entry from /proc/meminfo. */
     long getMemInfoEntry(String name);
diff --git a/tests/aidl/com/android/microdroid/testservice/ITestService.aidl b/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
index e8c435f..eda4f75 100644
--- a/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
+++ b/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
@@ -36,4 +36,7 @@
 
     /* get the APK contents path. */
     String getApkContentsPath();
+
+    /* get the encrypted storage path. */
+    String getEncryptedStoragePath();
 }
diff --git a/tests/benchmark/Android.bp b/tests/benchmark/Android.bp
index e6f39f8..1747183 100644
--- a/tests/benchmark/Android.bp
+++ b/tests/benchmark/Android.bp
@@ -16,7 +16,10 @@
         "com.android.microdroid.testservice-java",
         "truth-prebuilt",
     ],
-    libs: ["android.system.virtualmachine"],
+    // We need to compile against the .impl library which includes the hidden
+    // APIs. Once the APIs are promoted to @SystemApi we can switch to
+    // framework-virtualization, which contains API stubs.
+    libs: ["framework-virtualization.impl"],
     jni_libs: [
         "MicrodroidBenchmarkNativeLib",
         "MicrodroidIdleNativeLib",
@@ -28,20 +31,11 @@
 }
 
 cc_library_shared {
-    name: "MicrodroidIdleNativeLib",
-    srcs: ["src/native/idlebinary.cpp"],
-    header_libs: ["vm_payload_headers"],
-    shared_libs: [
-        "libbase",
-    ],
-}
-
-cc_library_shared {
     name: "MicrodroidBenchmarkNativeLib",
-    srcs: ["src/native/benchmarkbinary.cpp"],
+    srcs: ["src/native/*.cpp"],
+    local_include_dirs: ["src/native/include"],
     static_libs: [
         "com.android.microdroid.testservice-ndk",
-        "libiobenchmark",
     ],
     shared_libs: [
         "libbase",
@@ -50,12 +44,3 @@
         "libvm_payload",
     ],
 }
-
-cc_library {
-    name: "libiobenchmark",
-    srcs: ["src/native/io_vsock.cpp"],
-    export_include_dirs: ["src/native/include"],
-    shared_libs: [
-        "libbase",
-    ],
-}
diff --git a/tests/benchmark/AndroidManifest.xml b/tests/benchmark/AndroidManifest.xml
index c39b91c..8a7366a 100644
--- a/tests/benchmark/AndroidManifest.xml
+++ b/tests/benchmark/AndroidManifest.xml
@@ -18,7 +18,6 @@
     <uses-permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE" />
     <uses-permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE" />
     <application>
-        <uses-library android:name="android.system.virtualmachine" android:required="false" />
     </application>
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
         android:targetPackage="com.android.microdroid.benchmark"
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 db74358..28852e8 100644
--- a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
+++ b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
@@ -257,18 +257,10 @@
     private static class VirtioBlkListener implements BenchmarkVmListener.InnerListener {
         private static final String FILENAME = APEX_ETC_FS + "microdroid_super.img";
 
-        private final long mFileSizeBytes;
         private final List<Double> mReadRates;
         private final boolean mIsRand;
 
         VirtioBlkListener(List<Double> readRates, boolean isRand) {
-            File file = new File(FILENAME);
-            try {
-                mFileSizeBytes = Files.size(file.toPath());
-            } catch (IOException e) {
-                throw new RuntimeException(e);
-            }
-            assertThat(mFileSizeBytes).isGreaterThan((long) SIZE_MB);
             mReadRates = readRates;
             mIsRand = isRand;
         }
@@ -276,7 +268,7 @@
         @Override
         public void onPayloadReady(VirtualMachine vm, IBenchmarkService benchmarkService)
                 throws RemoteException {
-            double readRate = benchmarkService.measureReadRate(FILENAME, mFileSizeBytes, mIsRand);
+            double readRate = benchmarkService.measureReadRate(FILENAME, mIsRand);
             mReadRates.add(readRate);
         }
     }
diff --git a/tests/benchmark/src/native/benchmarkbinary.cpp b/tests/benchmark/src/native/benchmarkbinary.cpp
index 6321c25..e43025c 100644
--- a/tests/benchmark/src/native/benchmarkbinary.cpp
+++ b/tests/benchmark/src/native/benchmarkbinary.cpp
@@ -23,6 +23,8 @@
 #include <fcntl.h>
 #include <linux/vm_sockets.h>
 #include <stdio.h>
+#include <sys/stat.h>
+#include <sys/types.h>
 #include <time.h>
 #include <unistd.h>
 #include <vm_main.h>
@@ -56,9 +58,9 @@
 
 class IOBenchmarkService : public aidl::com::android::microdroid::testservice::BnBenchmarkService {
 public:
-    ndk::ScopedAStatus measureReadRate(const std::string& filename, int64_t fileSizeBytes,
-                                       bool isRand, double* out) override {
-        auto res = measure_read_rate(filename, fileSizeBytes, isRand);
+    ndk::ScopedAStatus measureReadRate(const std::string& filename, bool isRand,
+                                       double* out) override {
+        auto res = measure_read_rate(filename, isRand);
         if (res.ok()) {
             *out = res.value();
         }
@@ -90,13 +92,20 @@
     }
 
 private:
-    /** Measures the read rate for reading the given file. */
-    Result<double> measure_read_rate(const std::string& filename, int64_t fileSizeBytes,
-                                     bool is_rand) {
-        const int64_t block_count = fileSizeBytes / kBlockSizeBytes;
+    /**
+     * Measures the read rate for reading the given file.
+     * @return The read rate in MB/s.
+     */
+    Result<double> measure_read_rate(const std::string& filename, bool is_rand) {
+        struct stat file_stats;
+        if (stat(filename.c_str(), &file_stats) == -1) {
+            return Error() << "failed to get file stats";
+        }
+        const int64_t file_size_bytes = file_stats.st_size;
+        const int64_t block_count = file_size_bytes / kBlockSizeBytes;
         std::vector<uint64_t> offsets(block_count);
         for (auto i = 0; i < block_count; ++i) {
-            offsets.push_back(i * kBlockSizeBytes);
+            offsets[i] = i * kBlockSizeBytes;
         }
         if (is_rand) {
             std::mt19937 rd{std::random_device{}()};
@@ -118,8 +127,8 @@
             }
         }
         double elapsed_seconds = ((double)clock() - start) / CLOCKS_PER_SEC;
-        double read_rate = (double)fileSizeBytes / kNumBytesPerMB / elapsed_seconds;
-        return {read_rate};
+        double file_size_mb = (double)file_size_bytes / kNumBytesPerMB;
+        return {file_size_mb / elapsed_seconds};
     }
 
     Result<size_t> read_meminfo_entry(const std::string& stat) {
diff --git a/tests/helper/Android.bp b/tests/helper/Android.bp
index 60d4be1..7473dab 100644
--- a/tests/helper/Android.bp
+++ b/tests/helper/Android.bp
@@ -3,15 +3,12 @@
 }
 
 java_library_static {
-    name: "VirtualizationTestHelper",
-    srcs: ["src/java/com/android/virt/**/*.java"],
-    host_supported: true,
-}
-
-java_library_static {
     name: "MicrodroidTestHelper",
     srcs: ["src/java/com/android/microdroid/test/common/*.java"],
     host_supported: true,
+    libs: [
+        "framework-annotations-lib",
+    ],
 }
 
 java_library_static {
@@ -21,8 +18,10 @@
         "androidx.test.runner",
         "androidx.test.ext.junit",
         "MicrodroidTestHelper",
-        "VirtualizationTestHelper",
         "truth-prebuilt",
     ],
-    libs: ["android.system.virtualmachine"],
+    // We need to compile against the .impl library which includes the hidden
+    // APIs. Once the APIs are promoted to @SystemApi we can switch to
+    // framework-virtualization, which contains API stubs.
+    libs: ["framework-virtualization.impl"],
 }
diff --git a/tests/helper/src/java/com/android/microdroid/test/common/DeviceProperties.java b/tests/helper/src/java/com/android/microdroid/test/common/DeviceProperties.java
new file mode 100644
index 0000000..1fc163b
--- /dev/null
+++ b/tests/helper/src/java/com/android/microdroid/test/common/DeviceProperties.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.microdroid.test.common;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+/** This class can be used in both host tests and device tests to get the device properties. */
+public final class DeviceProperties {
+    /** PropertyGetter is used to get the property associated to a given key. */
+    public interface PropertyGetter {
+        @Nullable
+        String getProperty(@NonNull String key) throws Exception;
+    }
+
+    private static final String KEY_VENDOR_DEVICE = "ro.product.vendor.device";
+    private static final String KEY_METRICS_TAG = "debug.hypervisor.metrics_tag";
+
+    private static final String CUTTLEFISH_DEVICE_PREFIX = "vsoc_";
+
+    @NonNull private final PropertyGetter mPropertyGetter;
+
+    private DeviceProperties(@NonNull PropertyGetter propertyGetter) {
+        mPropertyGetter = requireNonNull(propertyGetter);
+    }
+
+    /** Creates a new instance of {@link DeviceProperties}. */
+    @NonNull
+    public static DeviceProperties create(@NonNull PropertyGetter propertyGetter) {
+        return new DeviceProperties(propertyGetter);
+    }
+
+    /**
+     * @return whether the device is a cuttlefish device.
+     */
+    public boolean isCuttlefish() {
+        String vendorDeviceName = getProperty(KEY_VENDOR_DEVICE);
+        return vendorDeviceName != null && vendorDeviceName.startsWith(CUTTLEFISH_DEVICE_PREFIX);
+    }
+
+    @Nullable
+    public String getMetricsTag() {
+        return getProperty(KEY_METRICS_TAG);
+    }
+
+    private String getProperty(String key) {
+        try {
+            return mPropertyGetter.getProperty(key);
+        } catch (Exception e) {
+            throw new IllegalArgumentException("Cannot get property for the key: " + key, e);
+        }
+    }
+}
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 9fb7d91..24e2049 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
@@ -15,9 +15,9 @@
  */
 package com.android.microdroid.test.device;
 
-import static com.google.common.truth.TruthJUnit.assume;
+import static android.content.pm.PackageManager.FEATURE_VIRTUALIZATION_FRAMEWORK;
 
-import static org.junit.Assume.assumeNoException;
+import static com.google.common.truth.TruthJUnit.assume;
 
 import android.app.Instrumentation;
 import android.app.UiAutomation;
@@ -35,8 +35,8 @@
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import com.android.microdroid.test.common.DeviceProperties;
 import com.android.microdroid.test.common.MetricsProcessor;
-import com.android.virt.VirtualizationTestHelper;
 
 import java.io.BufferedReader;
 import java.io.ByteArrayOutputStream;
@@ -51,13 +51,12 @@
 
 public abstract class MicrodroidDeviceTestBase {
     public static boolean isCuttlefish() {
-        return VirtualizationTestHelper.isCuttlefish(
-                SystemProperties.get("ro.product.vendor.device"));
+        return DeviceProperties.create(SystemProperties::get).isCuttlefish();
     }
 
     public static String getMetricPrefix() {
         return MetricsProcessor.getMetricPrefix(
-                SystemProperties.get("debug.hypervisor.metrics_tag"));
+                DeviceProperties.create(SystemProperties::get).getMetricsTag());
     }
 
     protected final void grantPermission(String permission) {
@@ -119,17 +118,12 @@
     }
 
     public void prepareTestSetup(boolean protectedVm) {
-        // In case when the virt APEX doesn't exist on the device, classes in the
-        // android.system.virtualmachine package can't be loaded. Therefore, before using the
-        // classes, check the existence of a class in the package and skip this test if not exist.
-        try {
-            Class.forName("android.system.virtualmachine.VirtualMachineManager");
-        } catch (ClassNotFoundException e) {
-            assumeNoException(e);
-            return;
-        }
-        Context context = ApplicationProvider.getApplicationContext();
-        mInner = new Inner(context, protectedVm, VirtualMachineManager.getInstance(context));
+        Context ctx = ApplicationProvider.getApplicationContext();
+        assume().withMessage("Device doesn't support AVF")
+                .that(ctx.getPackageManager().hasSystemFeature(FEATURE_VIRTUALIZATION_FRAMEWORK))
+                .isTrue();
+
+        mInner = new Inner(ctx, protectedVm, VirtualMachineManager.getInstance(ctx));
 
         int capabilities = mInner.getVirtualMachineManager().getCapabilities();
         if (protectedVm) {
@@ -232,7 +226,7 @@
         }
 
         @Override
-        public void onPayloadStarted(VirtualMachine vm, ParcelFileDescriptor stream) {}
+        public void onPayloadStarted(VirtualMachine vm) {}
 
         @Override
         public void onPayloadReady(VirtualMachine vm) {}
@@ -327,7 +321,7 @@
         VmEventListener listener =
                 new VmEventListener() {
                     @Override
-                    public void onPayloadStarted(VirtualMachine vm, ParcelFileDescriptor stream) {
+                    public void onPayloadStarted(VirtualMachine vm) {
                         endTime.complete(System.nanoTime());
                         payloadStarted.complete(true);
                         forceStop(vm);
diff --git a/tests/hostside/helper/Android.bp b/tests/hostside/helper/Android.bp
index b2333ab..6196ec5 100644
--- a/tests/hostside/helper/Android.bp
+++ b/tests/hostside/helper/Android.bp
@@ -12,6 +12,5 @@
     ],
     static_libs: [
         "MicrodroidTestHelper",
-        "VirtualizationTestHelper",
     ],
 }
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 43fe615..b4b3795 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
@@ -26,6 +26,7 @@
 import static org.junit.Assume.assumeTrue;
 
 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.microdroid.test.common.DeviceProperties;
 import com.android.microdroid.test.common.MetricsProcessor;
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.device.DeviceNotAvailableException;
@@ -34,7 +35,6 @@
 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
 import com.android.tradefed.util.CommandResult;
 import com.android.tradefed.util.RunUtil;
-import com.android.virt.VirtualizationTestHelper;
 
 import java.io.File;
 import java.io.FileNotFoundException;
@@ -49,10 +49,6 @@
     private static final String MICRODROID_SERIAL = "localhost:" + TEST_VM_ADB_PORT;
     private static final String INSTANCE_IMG = "instance.img";
 
-    // This is really slow on GCE (2m 40s) but fast on localhost or actual Android phones (< 10s).
-    // Then there is time to run the actual task. Set the maximum timeout value big enough.
-    private static final long MICRODROID_MAX_LIFETIME_MINUTES = 20;
-
     private static final long MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES = 5;
     protected static final long MICRODROID_COMMAND_TIMEOUT_MILLIS = 30000;
     private static final long MICRODROID_COMMAND_RETRY_INTERVAL_MILLIS = 500;
@@ -87,19 +83,21 @@
         android.tryRun("rm", "-rf", "/data/misc/virtualizationservice/*");
     }
 
-    protected boolean isCuttlefish() throws Exception {
-        return VirtualizationTestHelper.isCuttlefish(
-            getDevice().getProperty("ro.product.vendor.device"));
+    protected boolean isCuttlefish() {
+        return DeviceProperties.create(getDevice()::getProperty).isCuttlefish();
     }
 
-    protected String getMetricPrefix() throws Exception {
+    protected String getMetricPrefix() {
         return MetricsProcessor.getMetricPrefix(
-                getDevice().getProperty("debug.hypervisor.metrics_tag"));
+                DeviceProperties.create(getDevice()::getProperty).getMetricsTag());
     }
 
     public static void testIfDeviceIsCapable(ITestDevice androidDevice) throws Exception {
         assumeTrue("Need an actual TestDevice", androidDevice instanceof TestDevice);
         TestDevice testDevice = (TestDevice) androidDevice;
+        assumeTrue(
+                "Requires VM support",
+                testDevice.hasFeature("android.software.virtualization_framework"));
         assumeTrue("Requires VM support", testDevice.supportsMicrodroid());
     }
 
@@ -193,17 +191,6 @@
         return pathLine.substring("package:".length());
     }
 
-    private static void forwardFileToLog(CommandRunner android, String path, String tag)
-            throws DeviceNotAvailableException {
-        android.runWithTimeout(
-                MICRODROID_MAX_LIFETIME_MINUTES * 60 * 1000,
-                "logwrapper",
-                "sh",
-                "-c",
-                "\"$'tail -f -n +0 " + path
-                        + " | sed \\'s/^/" + tag + ": /g\\''\""); // add tags in front of lines
-    }
-
     public static void shutdownMicrodroid(ITestDevice androidDevice, String cid)
             throws DeviceNotAvailableException {
         CommandRunner android = new CommandRunner(androidDevice);
diff --git a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
index a836559..4e9c501 100644
--- a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
+++ b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
@@ -42,6 +42,7 @@
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.TestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestMetrics;
 import com.android.tradefed.util.CommandResult;
@@ -62,13 +63,19 @@
 import org.xml.sax.Attributes;
 import org.xml.sax.helpers.DefaultHandler;
 
+import java.io.BufferedReader;
 import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
 import java.util.function.Function;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -266,11 +273,11 @@
         return new ActiveApexInfoList(list);
     }
 
-    private String runMicrodroidWithResignedImages(
+    private Process runMicrodroidWithResignedImages(
             File key,
             Map<String, File> keyOverrides,
             boolean isProtected,
-            boolean daemonize,
+            boolean waitForOnline,
             String consolePath)
             throws Exception {
         CommandRunner android = new CommandRunner(getDevice());
@@ -375,19 +382,49 @@
         final String configPath = TEST_ROOT + "raw_config.json";
         getDevice().pushString(config.toString(), configPath);
 
-        final String logPath = LOG_PATH;
-        final String ret =
-                android.runWithTimeout(
-                        60 * 1000,
+        List<String> args =
+                Arrays.asList(
+                        "adb",
+                        "-s",
+                        getDevice().getSerialNumber(),
+                        "shell",
                         VIRT_APEX + "bin/vm run",
-                        daemonize ? "--daemonize" : "",
                         (consolePath != null) ? "--console " + consolePath : "",
-                        "--log " + logPath,
+                        "--log " + LOG_PATH,
                         configPath);
+
+        PipedInputStream pis = new PipedInputStream();
+        Process process = RunUtil.getDefault().runCmdInBackground(args, new PipedOutputStream(pis));
+        BufferedReader stdout = new BufferedReader(new InputStreamReader(pis));
+
+        // Retrieve the CID from the vm tool output
+        String cid = null;
         Pattern pattern = Pattern.compile("with CID (\\d+)");
-        Matcher matcher = pattern.matcher(ret);
-        assertWithMessage("Failed to find CID").that(matcher.find()).isTrue();
-        return matcher.group(1);
+        try {
+            String line;
+            while ((line = stdout.readLine()) != null) {
+                CLog.i("VM output: " + line);
+                Matcher matcher = pattern.matcher(line);
+                if (matcher.find()) {
+                    cid = matcher.group(1);
+                    break;
+                }
+            }
+        } catch (IOException ex) {
+            throw new IllegalStateException(
+                    "Could not find the CID of the VM. The process probably died.", ex);
+        }
+        if (cid == null) {
+            throw new IllegalStateException(
+                    "Could not find the CID of the VM. Output does not contain the expected"
+                            + " pattern.");
+        }
+
+        if (waitForOnline) {
+            adbConnectToMicrodroid(getDevice(), cid);
+        }
+
+        return process;
     }
 
     @Test
@@ -400,9 +437,12 @@
         File key = findTestFile("test.com.android.virt.pem");
         Map<String, File> keyOverrides = Map.of();
         boolean isProtected = true;
-        boolean daemonize = false; // VM should shut down due to boot failure.
+        boolean waitForOnline = false; // VM should shut down due to boot failure.
         String consolePath = TEST_ROOT + "console";
-        runMicrodroidWithResignedImages(key, keyOverrides, isProtected, daemonize, consolePath);
+        Process process =
+                runMicrodroidWithResignedImages(
+                        key, keyOverrides, isProtected, waitForOnline, consolePath);
+        process.waitFor(5L, TimeUnit.SECONDS);
         assertThat(getDevice().pullFileContents(consolePath), containsString("pvmfw boot failed"));
     }
 
@@ -416,14 +456,12 @@
         File key = findTestFile("test.com.android.virt.pem");
         Map<String, File> keyOverrides = Map.of();
         boolean isProtected = false;
-        boolean daemonize = true;
+        boolean waitForOnline = true; // Device online means that boot must have succeeded.
         String consolePath = TEST_ROOT + "console";
-        String cid =
+        Process process =
                 runMicrodroidWithResignedImages(
-                        key, keyOverrides, isProtected, daemonize, consolePath);
-        // Adb connection to the microdroid means that boot succeeded.
-        adbConnectToMicrodroid(getDevice(), cid);
-        shutdownMicrodroid(getDevice(), cid);
+                        key, keyOverrides, isProtected, waitForOnline, consolePath);
+        process.destroy();
     }
 
     @Test
@@ -434,18 +472,18 @@
         File key2 = findTestFile("test2.com.android.virt.pem");
         Map<String, File> keyOverrides = Map.of("microdroid_vbmeta.img", key2);
         boolean isProtected = false; // Not interested in pvwfw
-        boolean daemonize = true; // Bootloader fails and enters prompts.
+        boolean waitForOnline = false; // Bootloader fails and enters prompts.
         // To be able to stop it, it should be a daemon.
         String consolePath = TEST_ROOT + "console";
-        String cid =
+        Process process =
                 runMicrodroidWithResignedImages(
-                        key, keyOverrides, isProtected, daemonize, consolePath);
+                        key, keyOverrides, isProtected, waitForOnline, consolePath);
         // Wait so that init can print errors to console (time in cuttlefish >> in real device)
         assertThatEventually(
                 100000,
                 () -> getDevice().pullFileContents(consolePath),
                 containsString("init: [libfs_avb]Failed to verify vbmeta digest"));
-        shutdownMicrodroid(getDevice(), cid);
+        process.destroy();
     }
 
     private boolean isTombstoneGeneratedWithConfig(String configPath) throws Exception {
@@ -602,6 +640,7 @@
         // Check that no denials have happened so far
         CommandRunner android = new CommandRunner(getDevice());
         assertThat(android.tryRun("egrep", "'avc:[[:space:]]{1,2}denied'", LOG_PATH)).isNull();
+        assertThat(android.tryRun("egrep", "'avc:[[:space:]]{1,2}denied'", CONSOLE_PATH)).isNull();
 
         assertThat(microdroid.run("cat /proc/cpuinfo | grep processor | wc -l"))
                 .isEqualTo(Integer.toString(NUM_VCPUS));
diff --git a/tests/testapk/Android.bp b/tests/testapk/Android.bp
index 8972046..707dca1 100644
--- a/tests/testapk/Android.bp
+++ b/tests/testapk/Android.bp
@@ -19,7 +19,10 @@
         "truth-prebuilt",
         "compatibility-common-util-devicesidelib",
     ],
-    libs: ["android.system.virtualmachine"],
+    // We need to compile against the .impl library which includes the hidden
+    // APIs. Once the APIs are promoted to @SystemApi we can switch to
+    // framework-virtualization, which contains API stubs.
+    libs: ["framework-virtualization.impl"],
     jni_libs: [
         "MicrodroidTestNativeLib",
         "MicrodroidIdleNativeLib",
@@ -55,3 +58,10 @@
     srcs: ["src/native/testlib.cpp"],
     stl: "libc++_static",
 }
+
+cc_library_shared {
+    name: "MicrodroidIdleNativeLib",
+    srcs: ["src/native/idlebinary.cpp"],
+    header_libs: ["vm_payload_headers"],
+    stl: "libc++_static",
+}
diff --git a/tests/testapk/AndroidManifest.xml b/tests/testapk/AndroidManifest.xml
index ab22546..fefd20a 100644
--- a/tests/testapk/AndroidManifest.xml
+++ b/tests/testapk/AndroidManifest.xml
@@ -18,8 +18,8 @@
     <uses-permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE" />
     <uses-permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE" />
     <uses-sdk android:minSdkVersion="33" android:targetSdkVersion="33" />
+    <uses-feature android:name="android.software.virtualization_framework" android:required="false" />
     <application>
-        <uses-library android:name="android.system.virtualmachine" android:required="false" />
     </application>
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
         android:targetPackage="com.android.microdroid.test"
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 0e9ba55..da7c7ec 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -27,7 +27,6 @@
 
 import android.content.Context;
 import android.os.Build;
-import android.os.ParcelFileDescriptor;
 import android.os.ServiceSpecificException;
 import android.os.SystemProperties;
 import android.system.virtualmachine.VirtualMachine;
@@ -61,6 +60,7 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.Arrays;
 import java.util.List;
 import java.util.OptionalLong;
 import java.util.UUID;
@@ -121,6 +121,7 @@
         assertThat(testResults.mAppRunProp).isEqualTo("true");
         assertThat(testResults.mSublibRunProp).isEqualTo("true");
         assertThat(testResults.mApkContentsPath).isEqualTo("/mnt/apk");
+        assertThat(testResults.mEncryptedStoragePath).isEqualTo("");
     }
 
     @Test
@@ -600,31 +601,77 @@
     }
 
     @Test
-    public void vmConvertsToValidDescriptor() throws Exception {
+    public void importedVmAndOriginalVmHaveTheSameCdi() throws Exception {
+        assumeSupportedKernel();
+        // Arrange
+        grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
+        VirtualMachineConfig config =
+                mInner.newVmConfigBuilder()
+                        .setPayloadConfigPath("assets/vm_config.json")
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .build();
+        String vmNameOrig = "test_vm_orig";
+        String vmNameImport = "test_vm_import";
+        VirtualMachine vmOrig = mInner.forceCreateNewVirtualMachine(vmNameOrig, config);
+        VmCdis origCdis = launchVmAndGetCdis(vmNameOrig);
+        assertThat(origCdis.instanceSecret).isNotNull();
+        VirtualMachineDescriptor descriptor = vmOrig.toDescriptor();
+        VirtualMachineManager vmm = mInner.getVirtualMachineManager();
+        if (vmm.get(vmNameImport) != null) {
+            vmm.delete(vmNameImport);
+        }
+
+        // Action
+        // The imported VM will be fetched by name later.
+        VirtualMachine unusedVmImport = vmm.importFromDescriptor(vmNameImport, descriptor);
+
+        // Asserts
+        VmCdis importCdis = launchVmAndGetCdis(vmNameImport);
+        assertThat(origCdis.instanceSecret).isEqualTo(importCdis.instanceSecret);
+    }
+
+    @Test
+    public void importedVmIsEqualToTheOriginalVm() throws Exception {
         // Arrange
         VirtualMachineConfig config =
                 mInner.newVmConfigBuilder()
                         .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
                         .setDebugLevel(DEBUG_LEVEL_NONE)
                         .build();
-        String vmName = "test_vm";
-        VirtualMachine vm = mInner.forceCreateNewVirtualMachine(vmName, config);
+        String vmNameOrig = "test_vm_orig";
+        String vmNameImport = "test_vm_import";
+        VirtualMachine vmOrig = mInner.forceCreateNewVirtualMachine(vmNameOrig, config);
+        // Run something to make the instance.img different with the initialized one.
+        TestResults origTestResults = runVmTestService(vmOrig);
+        assertThat(origTestResults.mException).isNull();
+        assertThat(origTestResults.mAddInteger).isEqualTo(123 + 456);
+        VirtualMachineDescriptor descriptor = vmOrig.toDescriptor();
+        VirtualMachineManager vmm = mInner.getVirtualMachineManager();
+        if (vmm.get(vmNameImport) != null) {
+            vmm.delete(vmNameImport);
+        }
 
         // Action
-        VirtualMachineDescriptor descriptor = vm.toDescriptor();
+        VirtualMachine vmImport = vmm.importFromDescriptor(vmNameImport, descriptor);
 
         // Asserts
-        assertFileContentsAreEqual(descriptor.getConfigFd(), vmName, "config.xml");
-        assertFileContentsAreEqual(descriptor.getInstanceImgFd(), vmName, "instance.img");
+        assertFileContentsAreEqualInTwoVms("config.xml", vmNameOrig, vmNameImport);
+        assertFileContentsAreEqualInTwoVms("instance.img", vmNameOrig, vmNameImport);
+        assertThat(vmImport).isNotEqualTo(vmOrig);
+        vmm.delete(vmNameOrig);
+        assertThat(vmImport).isEqualTo(vmm.get(vmNameImport));
+        TestResults testResults = runVmTestService(vmImport);
+        assertThat(testResults.mException).isNull();
+        assertThat(testResults.mAddInteger).isEqualTo(123 + 456);
     }
 
-    private void assertFileContentsAreEqual(
-            ParcelFileDescriptor parcelFd, String vmName, String fileName) throws IOException {
-        File file = getVmFile(vmName, fileName);
-        // Use try-with-resources to close the files automatically after assert.
-        try (FileInputStream input1 = new FileInputStream(parcelFd.getFileDescriptor());
-                FileInputStream input2 = new FileInputStream(file)) {
-            assertThat(input1.readAllBytes()).isEqualTo(input2.readAllBytes());
+    private void assertFileContentsAreEqualInTwoVms(String fileName, String vmName1, String vmName2)
+            throws IOException {
+        File file1 = getVmFile(vmName1, fileName);
+        File file2 = getVmFile(vmName2, fileName);
+        try (FileInputStream input1 = new FileInputStream(file1);
+                FileInputStream input2 = new FileInputStream(file2)) {
+            assertThat(Arrays.equals(input1.readAllBytes(), input2.readAllBytes())).isTrue();
         }
     }
 
@@ -661,6 +708,7 @@
         String mSublibRunProp;
         String mExtraApkTestProp;
         String mApkContentsPath;
+        String mEncryptedStoragePath;
     }
 
     private TestResults runVmTestService(VirtualMachine vm) throws Exception {
@@ -671,8 +719,9 @@
                 new VmEventListener() {
                     private void testVMService(VirtualMachine vm) {
                         try {
-                            ITestService testService = ITestService.Stub.asInterface(
-                                    vm.connectToVsockServer(ITestService.SERVICE_PORT));
+                            ITestService testService =
+                                    ITestService.Stub.asInterface(
+                                            vm.connectToVsockServer(ITestService.SERVICE_PORT));
                             testResults.mAddInteger = testService.addInteger(123, 456);
                             testResults.mAppRunProp =
                                     testService.readProperty("debug.microdroid.app.run");
@@ -681,6 +730,8 @@
                             testResults.mExtraApkTestProp =
                                     testService.readProperty("debug.microdroid.test.extra_apk");
                             testResults.mApkContentsPath = testService.getApkContentsPath();
+                            testResults.mEncryptedStoragePath =
+                                    testService.getEncryptedStoragePath();
                         } catch (Exception e) {
                             testResults.mException = e;
                         }
@@ -695,11 +746,9 @@
                     }
 
                     @Override
-                    public void onPayloadStarted(VirtualMachine vm, ParcelFileDescriptor stream) {
+                    public void onPayloadStarted(VirtualMachine vm) {
                         Log.i(TAG, "onPayloadStarted");
                         payloadStarted.complete(true);
-                        logVmOutput(TAG, new FileInputStream(stream.getFileDescriptor()),
-                                "Payload");
                     }
                 };
         listener.runToFinish(TAG, vm);
diff --git a/tests/benchmark/src/native/idlebinary.cpp b/tests/testapk/src/native/idlebinary.cpp
similarity index 100%
rename from tests/benchmark/src/native/idlebinary.cpp
rename to tests/testapk/src/native/idlebinary.cpp
diff --git a/tests/testapk/src/native/testbinary.cpp b/tests/testapk/src/native/testbinary.cpp
index 48942dc..694f452 100644
--- a/tests/testapk/src/native/testbinary.cpp
+++ b/tests/testapk/src/native/testbinary.cpp
@@ -123,6 +123,16 @@
             *out = path;
             return ndk::ScopedAStatus::ok();
         }
+
+        ndk::ScopedAStatus getEncryptedStoragePath(std::string* out) override {
+            const char* path_c = AVmPayload_getEncryptedStoragePath();
+            if (path_c == nullptr) {
+                out->clear();
+            } else {
+                *out = path_c;
+            }
+            return ndk::ScopedAStatus::ok();
+        }
     };
     auto testService = ndk::SharedRefBase::make<TestService>();
 
diff --git a/virtualizationservice/Android.bp b/virtualizationservice/Android.bp
index d6f4607..b767013 100644
--- a/virtualizationservice/Android.bp
+++ b/virtualizationservice/Android.bp
@@ -22,6 +22,7 @@
     rustlibs: [
         "android.system.virtualizationcommon-rust",
         "android.system.virtualizationservice-rust",
+        "android.system.virtualizationservice_internal-rust",
         "android.system.virtualmachineservice-rust",
         "android.os.permissions_aidl-rust",
         "libandroid_logger",
diff --git a/virtualizationservice/aidl/Android.bp b/virtualizationservice/aidl/Android.bp
index 4d5326a..a0bbc00 100644
--- a/virtualizationservice/aidl/Android.bp
+++ b/virtualizationservice/aidl/Android.bp
@@ -34,11 +34,31 @@
 }
 
 aidl_interface {
+    name: "android.system.virtualizationservice_internal",
+    srcs: ["android/system/virtualizationservice_internal/**/*.aidl"],
+    unstable: true,
+    backend: {
+        java: {
+            sdk_version: "module_current",
+        },
+        rust: {
+            enabled: true,
+            apex_available: [
+                "com.android.virt",
+            ],
+        },
+    },
+}
+
+aidl_interface {
     name: "android.system.virtualmachineservice",
     srcs: ["android/system/virtualmachineservice/**/*.aidl"],
     imports: ["android.system.virtualizationcommon"],
     unstable: true,
     backend: {
+        java: {
+            sdk_version: "module_current",
+        },
         rust: {
             enabled: true,
             apex_available: [
@@ -55,6 +75,7 @@
     unstable: true,
     backend: {
         java: {
+            sdk_version: "module_current",
             apex_available: ["com.android.virt"],
         },
         ndk: {
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachineCallback.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachineCallback.aidl
index 8d6ed08..a329fa6 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachineCallback.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachineCallback.aidl
@@ -24,13 +24,9 @@
  */
 oneway interface IVirtualMachineCallback {
     /**
-     * Called when the payload starts in the VM. `stream` is the input/output port of the payload.
-     *
-     * <p>Note: when the virtual machine object is shared to multiple processes and they register
-     * this callback to the same virtual machine object, the processes will compete to access the
-     * same payload stream. Keep only one process to access the stream.
+     * Called when the payload starts in the VM.
      */
-    void onPayloadStarted(int cid, in @nullable ParcelFileDescriptor stream);
+    void onPayloadStarted(int cid);
 
     /**
      * Called when the payload in the VM is ready to serve.
diff --git a/tests/helper/src/java/com/android/virt/VirtualizationTestHelper.java b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IGlobalVmContext.aidl
similarity index 66%
rename from tests/helper/src/java/com/android/virt/VirtualizationTestHelper.java
rename to virtualizationservice/aidl/android/system/virtualizationservice_internal/IGlobalVmContext.aidl
index 4c27915..1a7aa4a 100644
--- a/tests/helper/src/java/com/android/virt/VirtualizationTestHelper.java
+++ b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IGlobalVmContext.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright 2022 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -13,10 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.virt;
+package android.system.virtualizationservice_internal;
 
-public abstract class VirtualizationTestHelper {
-    public static boolean isCuttlefish(String vendorDeviceName) {
-        return vendorDeviceName != null && vendorDeviceName.startsWith("vsoc_");
-    }
+interface IGlobalVmContext {
+    /** Get the CID allocated to the VM. */
+    int getCid();
 }
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
new file mode 100644
index 0000000..851ddf4
--- /dev/null
+++ b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.system.virtualizationservice_internal;
+
+import android.system.virtualizationservice_internal.IGlobalVmContext;
+
+interface IVirtualizationServiceInternal {
+    /**
+     * Allocates global context for a new VM.
+     *
+     * This allocates VM's globally unique resources such as the CID.
+     * The resources will not be recycled as long as there is a strong reference
+     * to the returned object.
+     */
+    IGlobalVmContext allocateGlobalVmContext();
+}
diff --git a/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl b/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl
index e8c1724..f2d92af 100644
--- a/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl
+++ b/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl
@@ -21,12 +21,6 @@
 interface IVirtualMachineService {
     /**
      * Port number that VirtualMachineService listens on connections from the guest VMs for the
-     * payload input and output.
-     */
-    const int VM_STREAM_SERVICE_PORT = 3000;
-
-    /**
-     * Port number that VirtualMachineService listens on connections from the guest VMs for the
      * VirtualMachineService binder service.
      */
     const int VM_BINDER_SERVICE_PORT = 5000;
@@ -53,7 +47,7 @@
     void notifyPayloadFinished(int exitCode);
 
     /**
-     * Notifies that an error has occurred inside the VM..
+     * Notifies that an error has occurred inside the VM.
      */
     void notifyError(ErrorCode errorCode, in String message);
 }
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index bc697e3..cfcfa2b 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -36,9 +36,13 @@
     VirtualMachineRawConfig::VirtualMachineRawConfig,
     VirtualMachineState::VirtualMachineState,
 };
+use android_system_virtualizationservice_internal::aidl::android::system::virtualizationservice_internal::{
+    IGlobalVmContext::{BnGlobalVmContext, IGlobalVmContext},
+    IVirtualizationServiceInternal::{BnVirtualizationServiceInternal, IVirtualizationServiceInternal},
+};
 use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice::IVirtualMachineService::{
         BnVirtualMachineService, IVirtualMachineService, VM_BINDER_SERVICE_PORT,
-        VM_STREAM_SERVICE_PORT, VM_TOMBSTONES_SERVICE_PORT,
+        VM_TOMBSTONES_SERVICE_PORT,
 };
 use anyhow::{anyhow, bail, Context, Result};
 use apkverify::{HashAlgorithm, V4Signature};
@@ -94,10 +98,94 @@
 
 const MICRODROID_OS_NAME: &str = "microdroid";
 
-/// Implementation of `IVirtualizationService`, the entry point of the AIDL service.
+/// Singleton service for allocating globally-unique VM resources, such as the CID, and running
+/// singleton servers, like tombstone receiver.
 #[derive(Debug, Default)]
+pub struct VirtualizationServiceInternal {
+    state: Arc<Mutex<GlobalState>>,
+}
+
+impl VirtualizationServiceInternal {
+    pub fn init() -> VirtualizationServiceInternal {
+        let service = VirtualizationServiceInternal::default();
+
+        std::thread::spawn(|| {
+            if let Err(e) = handle_stream_connection_tombstoned() {
+                warn!("Error receiving tombstone from guest or writing them. Error: {:?}", e);
+            }
+        });
+
+        service
+    }
+}
+
+impl Interface for VirtualizationServiceInternal {}
+
+impl IVirtualizationServiceInternal for VirtualizationServiceInternal {
+    fn allocateGlobalVmContext(&self) -> binder::Result<Strong<dyn IGlobalVmContext>> {
+        let state = &mut *self.state.lock().unwrap();
+        let cid = state.allocate_cid().map_err(|e| {
+            Status::new_exception_str(ExceptionCode::ILLEGAL_STATE, Some(e.to_string()))
+        })?;
+        Ok(GlobalVmContext::create(cid))
+    }
+}
+
+/// The mutable state of the VirtualizationServiceInternal. There should only be one instance
+/// of this struct.
+#[derive(Debug, Default)]
+struct GlobalState {}
+
+impl GlobalState {
+    /// Get the next available CID, or an error if we have run out. The last CID used is stored in
+    /// a system property so that restart of virtualizationservice doesn't reuse CID while the host
+    /// Android is up.
+    fn allocate_cid(&mut self) -> Result<Cid> {
+        let cid = match system_properties::read(SYSPROP_LAST_CID)? {
+            Some(val) => match val.parse::<Cid>() {
+                Ok(num) => num.checked_add(1).ok_or_else(|| anyhow!("ran out of CIDs"))?,
+                Err(_) => {
+                    error!("Invalid value '{}' of property '{}'", val, SYSPROP_LAST_CID);
+                    FIRST_GUEST_CID
+                }
+            },
+            None => FIRST_GUEST_CID,
+        };
+        system_properties::write(SYSPROP_LAST_CID, &format!("{}", cid))?;
+        Ok(cid)
+    }
+}
+
+/// Implementation of the AIDL `IGlobalVmContext` interface.
+#[derive(Debug, Default)]
+struct GlobalVmContext {
+    /// The unique CID assigned to the VM for vsock communication.
+    cid: Cid,
+    /// Keeps our service process running as long as this VM instance exists.
+    #[allow(dead_code)]
+    lazy_service_guard: LazyServiceGuard,
+}
+
+impl GlobalVmContext {
+    fn create(cid: Cid) -> Strong<dyn IGlobalVmContext> {
+        let binder = GlobalVmContext { cid, ..Default::default() };
+        BnGlobalVmContext::new_binder(binder, BinderFeatures::default())
+    }
+}
+
+impl Interface for GlobalVmContext {}
+
+impl IGlobalVmContext for GlobalVmContext {
+    fn getCid(&self) -> binder::Result<i32> {
+        Ok(self.cid as i32)
+    }
+}
+
+/// Implementation of `IVirtualizationService`, the entry point of the AIDL service.
+#[derive(Debug)]
 pub struct VirtualizationService {
     state: Arc<Mutex<State>>,
+    global_service: Strong<dyn IVirtualizationServiceInternal>,
 }
 
 impl Interface for VirtualizationService {
@@ -299,19 +387,11 @@
 
 impl VirtualizationService {
     pub fn init() -> VirtualizationService {
-        let service = VirtualizationService::default();
+        let global_service = VirtualizationServiceInternal::init();
+        let global_service =
+            BnVirtualizationServiceInternal::new_binder(global_service, BinderFeatures::default());
 
-        // server for payload output
-        let state = service.state.clone(); // reference to state (not the state itself) is copied
-        std::thread::spawn(move || {
-            handle_stream_connection_from_vm(state).unwrap();
-        });
-
-        std::thread::spawn(|| {
-            if let Err(e) = handle_stream_connection_tombstoned() {
-                warn!("Error receiving tombstone from guest or writing them. Error: {:?}", e);
-            }
-        });
+        let service = VirtualizationService { global_service, state: Default::default() };
 
         // binder server for vm
         // reference to state (not the state itself) is copied
@@ -352,12 +432,14 @@
             check_use_custom_virtual_machine()?;
         }
 
+        let vm_context = self.global_service.allocateGlobalVmContext()?;
+        let cid = vm_context.getCid()? as Cid;
+
         let state = &mut *self.state.lock().unwrap();
         let console_fd = console_fd.map(clone_file).transpose()?;
         let log_fd = log_fd.map(clone_file).transpose()?;
         let requester_uid = ThreadState::get_calling_uid();
         let requester_debug_pid = ThreadState::get_calling_pid();
-        let cid = state.next_cid().or(Err(ExceptionCode::ILLEGAL_STATE))?;
 
         // Counter to generate unique IDs for temporary image files.
         let mut next_temporary_image_id = 0;
@@ -474,47 +556,26 @@
             detect_hangup: is_app_config,
         };
         let instance = Arc::new(
-            VmInstance::new(crosvm_config, temporary_directory, requester_uid, requester_debug_pid)
-                .map_err(|e| {
-                    error!("Failed to create VM with config {:?}: {:?}", config, e);
-                    Status::new_service_specific_error_str(
-                        -1,
-                        Some(format!("Failed to create VM: {:?}", e)),
-                    )
-                })?,
+            VmInstance::new(
+                crosvm_config,
+                temporary_directory,
+                requester_uid,
+                requester_debug_pid,
+                vm_context,
+            )
+            .map_err(|e| {
+                error!("Failed to create VM with config {:?}: {:?}", config, e);
+                Status::new_service_specific_error_str(
+                    -1,
+                    Some(format!("Failed to create VM: {:?}", e)),
+                )
+            })?,
         );
         state.add_vm(Arc::downgrade(&instance));
         Ok(VirtualMachine::create(instance))
     }
 }
 
-/// Waits for incoming connections from VM. If a new connection is made, stores the stream in the
-/// corresponding `VmInstance`.
-fn handle_stream_connection_from_vm(state: Arc<Mutex<State>>) -> Result<()> {
-    let listener =
-        VsockListener::bind_with_cid_port(VMADDR_CID_HOST, VM_STREAM_SERVICE_PORT as u32)?;
-    for stream in listener.incoming() {
-        let stream = match stream {
-            Err(e) => {
-                warn!("invalid incoming connection: {:?}", e);
-                continue;
-            }
-            Ok(s) => s,
-        };
-        if let Ok(addr) = stream.peer_addr() {
-            let cid = addr.cid();
-            let port = addr.port();
-            info!("payload stream connected from cid={}, port={}", cid, port);
-            if let Some(vm) = state.lock().unwrap().get_vm(cid) {
-                *vm.stream.lock().unwrap() = Some(stream);
-            } else {
-                error!("connection from cid={} is not from a guest VM", cid);
-            }
-        }
-    }
-    Ok(())
-}
-
 fn write_zero_filler(zero_filler_path: &Path) -> Result<()> {
     let file = OpenOptions::new()
         .create_new(true)
@@ -533,8 +594,7 @@
 }
 
 fn prepare_ramdump_file(ramdump_path: &Path) -> Result<File> {
-    File::create(&ramdump_path)
-        .context(format!("Failed to create ramdump file {:?}", &ramdump_path))
+    File::create(ramdump_path).context(format!("Failed to create ramdump file {:?}", &ramdump_path))
 }
 
 /// Given the configuration for a disk image, assembles the `DiskFile` to pass to crosvm.
@@ -854,11 +914,10 @@
 
 impl VirtualMachineCallbacks {
     /// Call all registered callbacks to notify that the payload has started.
-    pub fn notify_payload_started(&self, cid: Cid, stream: Option<VsockStream>) {
+    pub fn notify_payload_started(&self, cid: Cid) {
         let callbacks = &*self.0.lock().unwrap();
-        let pfd = stream.map(vsock_stream_to_pfd);
         for callback in callbacks {
-            if let Err(e) = callback.onPayloadStarted(cid as i32, pfd.as_ref()) {
+            if let Err(e) = callback.onPayloadStarted(cid as i32) {
                 error!("Error notifying payload start event from VM CID {}: {:?}", cid, e);
             }
         }
@@ -968,27 +1027,6 @@
         let vm = self.debug_held_vms.swap_remove(pos);
         Some(vm)
     }
-
-    /// Get the next available CID, or an error if we have run out. The last CID used is stored in
-    /// a system property so that restart of virtualizationservice doesn't reuse CID while the host
-    /// Android is up.
-    fn next_cid(&mut self) -> Result<Cid> {
-        let next = if let Some(val) = system_properties::read(SYSPROP_LAST_CID)? {
-            if let Ok(num) = val.parse::<u32>() {
-                num.checked_add(1).ok_or_else(|| anyhow!("run out of CID"))?
-            } else {
-                error!("Invalid last CID {}. Using {}", &val, FIRST_GUEST_CID);
-                FIRST_GUEST_CID
-            }
-        } else {
-            // First VM since the boot
-            FIRST_GUEST_CID
-        };
-        // Persist the last value for next use
-        let str_val = format!("{}", next);
-        system_properties::write(SYSPROP_LAST_CID, &str_val)?;
-        Ok(next)
-    }
 }
 
 /// Gets the `VirtualMachineState` of the given `VmInstance`.
@@ -1072,11 +1110,10 @@
             vm.update_payload_state(PayloadState::Started).map_err(|e| {
                 Status::new_exception_str(ExceptionCode::ILLEGAL_STATE, Some(e.to_string()))
             })?;
-            let stream = vm.stream.lock().unwrap().take();
-            vm.callbacks.notify_payload_started(cid, stream);
+            vm.callbacks.notify_payload_started(cid);
 
-            let vm_start_timestamp = vm.vm_start_timestamp.lock().unwrap();
-            write_vm_booted_stats(vm.requester_uid as i32, &vm.name, *vm_start_timestamp);
+            let vm_start_timestamp = vm.vm_metric.lock().unwrap().start_timestamp;
+            write_vm_booted_stats(vm.requester_uid as i32, &vm.name, vm_start_timestamp);
             Ok(())
         } else {
             error!("notifyPayloadStarted is called from an unknown CID {}", cid);
diff --git a/virtualizationservice/src/atom.rs b/virtualizationservice/src/atom.rs
index 20f88e7..9c74d1e 100644
--- a/virtualizationservice/src/atom.rs
+++ b/virtualizationservice/src/atom.rs
@@ -15,6 +15,7 @@
 //! Functions for creating and collecting atoms.
 
 use crate::aidl::clone_file;
+use crate::crosvm::VmMetric;
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
     DeathReason::DeathReason,
     IVirtualMachine::IVirtualMachine,
@@ -164,15 +165,18 @@
     uid: i32,
     vm_identifier: &str,
     reason: DeathReason,
-    vm_start_timestamp: Option<SystemTime>,
+    vm_metric: &VmMetric,
 ) {
     let vm_identifier = vm_identifier.to_owned();
-    let duration = get_duration(vm_start_timestamp);
+    let elapsed_time_millis = get_duration(vm_metric.start_timestamp).as_millis() as i64;
+    let guest_time_millis = vm_metric.cpu_guest_time.unwrap_or_default();
+    let rss = vm_metric.rss.unwrap_or_default();
+
     thread::spawn(move || {
         let vm_exited = vm_exited::VmExited {
             uid,
             vm_identifier: &vm_identifier,
-            elapsed_time_millis: duration.as_millis() as i64,
+            elapsed_time_millis,
             death_reason: match reason {
                 DeathReason::INFRASTRUCTURE_ERROR => vm_exited::DeathReason::InfrastructureError,
                 DeathReason::KILLED => vm_exited::DeathReason::Killed,
@@ -211,6 +215,9 @@
                 DeathReason::HANGUP => vm_exited::DeathReason::Hangup,
                 _ => vm_exited::DeathReason::Unknown,
             },
+            guest_time_millis,
+            rss_vm_kb: rss.vm,
+            rss_crosvm_kb: rss.crosvm,
         };
         wait_for_statsd().unwrap_or_else(|e| warn!("failed to wait for statsd with error: {}", e));
         match vm_exited.stats_write() {
diff --git a/virtualizationservice/src/composite.rs b/virtualizationservice/src/composite.rs
index c9a68ac..fe17ff4 100644
--- a/virtualizationservice/src/composite.rs
+++ b/virtualizationservice/src/composite.rs
@@ -51,7 +51,7 @@
         OpenOptions::new().create_new(true).read(true).write(true).open(footer_path).with_context(
             || format!("Failed to create composite image header {:?}", footer_path),
         )?;
-    let zero_filler_file = File::open(&zero_filler_path).with_context(|| {
+    let zero_filler_file = File::open(zero_filler_path).with_context(|| {
         format!("Failed to open composite image zero filler {:?}", zero_filler_path)
     })?;
 
@@ -66,7 +66,7 @@
     )?;
 
     // Re-open the composite image as read-only.
-    let composite_image = File::open(&output_path)
+    let composite_image = File::open(output_path)
         .with_context(|| format!("Failed to open composite image {:?}", output_path))?;
 
     files.push(header_file);
diff --git a/virtualizationservice/src/crosvm.rs b/virtualizationservice/src/crosvm.rs
index 1b8061e..68324c5 100644
--- a/virtualizationservice/src/crosvm.rs
+++ b/virtualizationservice/src/crosvm.rs
@@ -16,16 +16,18 @@
 
 use crate::aidl::{Cid, VirtualMachineCallbacks};
 use crate::atom::write_vm_exited_stats;
-use anyhow::{anyhow, bail, Context, Error};
+use anyhow::{anyhow, bail, Context, Error, Result};
 use command_fds::CommandFdExt;
 use lazy_static::lazy_static;
+use libc::{sysconf, _SC_CLK_TCK};
 use log::{debug, error, info};
 use semver::{Version, VersionReq};
 use nix::{fcntl::OFlag, unistd::pipe2};
 use regex::{Captures, Regex};
 use shared_child::SharedChild;
 use std::borrow::Cow;
-use std::fs::{remove_dir_all, File};
+use std::cmp::max;
+use std::fs::{read_to_string, remove_dir_all, File};
 use std::io::{self, Read};
 use std::mem;
 use std::num::NonZeroU32;
@@ -35,8 +37,8 @@
 use std::sync::{Arc, Condvar, Mutex};
 use std::time::{Duration, SystemTime};
 use std::thread;
-use vsock::VsockStream;
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::DeathReason::DeathReason;
+use android_system_virtualizationservice_internal::aidl::android::system::virtualizationservice_internal::IGlobalVmContext::IGlobalVmContext;
 use binder::Strong;
 use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice::IVirtualMachineService::IVirtualMachineService;
 use tombstoned_client::{TombstonedConnection, DebuggerdDumpType};
@@ -61,6 +63,8 @@
 /// The exit status which crosvm returns when vcpu is stalled.
 const CROSVM_WATCHDOG_REBOOT_STATUS: i32 = 36;
 
+const MILLIS_PER_SEC: i64 = 1000;
+
 lazy_static! {
     /// If the VM doesn't move to the Started state within this amount time, a hang-up error is
     /// triggered.
@@ -133,6 +137,24 @@
     Failed,
 }
 
+/// RSS values of VM and CrosVM process itself.
+#[derive(Copy, Clone, Debug, Default)]
+pub struct Rss {
+    pub vm: i64,
+    pub crosvm: i64,
+}
+
+/// Metrics regarding the VM.
+#[derive(Debug, Default)]
+pub struct VmMetric {
+    /// Recorded timestamp when the VM is started.
+    pub start_timestamp: Option<SystemTime>,
+    /// Update most recent guest_time periodically from /proc/[crosvm pid]/stat while VM is running.
+    pub cpu_guest_time: Option<i64>,
+    /// Update maximum RSS values periodically from /proc/[crosvm pid]/smaps while VM is running.
+    pub rss: Option<Rss>,
+}
+
 impl VmState {
     /// Tries to start the VM, if it is in the `NotStarted` state.
     ///
@@ -147,6 +169,12 @@
             let child =
                 Arc::new(run_vm(config, &instance.temporary_directory, failure_pipe_write)?);
 
+            let instance_monitor_status = instance.clone();
+            let child_monitor_status = child.clone();
+            thread::spawn(move || {
+                instance_monitor_status.clone().monitor_vm_status(child_monitor_status);
+            });
+
             let child_clone = child.clone();
             let instance_clone = instance.clone();
             thread::spawn(move || {
@@ -175,6 +203,9 @@
 pub struct VmInstance {
     /// The current state of the VM.
     pub vm_state: Mutex<VmState>,
+    /// Handle to global resources allocated for this VM.
+    #[allow(dead_code)] // The handle is never read, we only need to hold it.
+    vm_context: Strong<dyn IGlobalVmContext>,
     /// The CID assigned to the VM for vsock communication.
     pub cid: Cid,
     /// The name of the VM.
@@ -190,12 +221,10 @@
     pub requester_debug_pid: i32,
     /// Callbacks to clients of the VM.
     pub callbacks: VirtualMachineCallbacks,
-    /// Input/output stream of the payload run in the VM.
-    pub stream: Mutex<Option<VsockStream>>,
     /// VirtualMachineService binder object for the VM.
     pub vm_service: Mutex<Option<Strong<dyn IVirtualMachineService>>>,
-    /// Recorded timestamp when the VM is started.
-    pub vm_start_timestamp: Mutex<Option<SystemTime>>,
+    /// Recorded metrics of VM such as timestamp or cpu / memory usage.
+    pub vm_metric: Mutex<VmMetric>,
     /// The latest lifecycle state which the payload reported itself to be in.
     payload_state: Mutex<PayloadState>,
     /// Represents the condition that payload_state was updated
@@ -209,6 +238,7 @@
         temporary_directory: PathBuf,
         requester_uid: u32,
         requester_debug_pid: i32,
+        vm_context: Strong<dyn IGlobalVmContext>,
     ) -> Result<VmInstance, Error> {
         validate_config(&config)?;
         let cid = config.cid;
@@ -216,6 +246,7 @@
         let protected = config.protected;
         Ok(VmInstance {
             vm_state: Mutex::new(VmState::NotStarted { config }),
+            vm_context,
             cid,
             name,
             protected,
@@ -223,9 +254,8 @@
             requester_uid,
             requester_debug_pid,
             callbacks: Default::default(),
-            stream: Mutex::new(None),
             vm_service: Mutex::new(None),
-            vm_start_timestamp: Mutex::new(None),
+            vm_metric: Mutex::new(Default::default()),
             payload_state: Mutex::new(PayloadState::Starting),
             payload_state_updated: Condvar::new(),
         })
@@ -234,7 +264,8 @@
     /// Starts an instance of `crosvm` to manage the VM. The `crosvm` instance will be killed when
     /// the `VmInstance` is dropped.
     pub fn start(self: &Arc<Self>) -> Result<(), Error> {
-        *self.vm_start_timestamp.lock().unwrap() = Some(SystemTime::now());
+        let mut vm_metric = self.vm_metric.lock().unwrap();
+        vm_metric.start_timestamp = Some(SystemTime::now());
         self.vm_state.lock().unwrap().start(self.clone())
     }
 
@@ -283,13 +314,8 @@
         let death_reason = death_reason(&result, &failure_reason);
         self.callbacks.callback_on_died(self.cid, death_reason);
 
-        let vm_start_timestamp = self.vm_start_timestamp.lock().unwrap();
-        write_vm_exited_stats(
-            self.requester_uid as i32,
-            &self.name,
-            death_reason,
-            *vm_start_timestamp,
-        );
+        let vm_metric = self.vm_metric.lock().unwrap();
+        write_vm_exited_stats(self.requester_uid as i32, &self.name, death_reason, &*vm_metric);
 
         // Delete temporary files.
         if let Err(e) = remove_dir_all(&self.temporary_directory) {
@@ -321,6 +347,42 @@
         }
     }
 
+    fn monitor_vm_status(&self, child: Arc<SharedChild>) {
+        let pid = child.id();
+
+        loop {
+            {
+                // Check VM state
+                let vm_state = &*self.vm_state.lock().unwrap();
+                if let VmState::Dead = vm_state {
+                    break;
+                }
+
+                let mut vm_metric = self.vm_metric.lock().unwrap();
+
+                // Get CPU Information
+                // TODO: Collect it once right before VM dies using SIGCHLD
+                if let Ok(guest_time) = get_guest_time(pid) {
+                    vm_metric.cpu_guest_time = Some(guest_time);
+                } else {
+                    error!("Failed to parse /proc/[pid]/stat");
+                }
+
+                // Get Memory Information
+                if let Ok(rss) = get_rss(pid) {
+                    vm_metric.rss = match &vm_metric.rss {
+                        Some(x) => Some(Rss::extract_max(x, &rss)),
+                        None => Some(rss),
+                    }
+                } else {
+                    error!("Failed to parse /proc/[pid]/smaps");
+                }
+            }
+
+            thread::sleep(Duration::from_secs(1));
+        }
+    }
+
     /// Returns the last reported state of the VM payload.
     pub fn payload_state(&self) -> PayloadState {
         *self.payload_state.lock().unwrap()
@@ -391,6 +453,60 @@
     }
 }
 
+impl Rss {
+    fn extract_max(x: &Rss, y: &Rss) -> Rss {
+        Rss { vm: max(x.vm, y.vm), crosvm: max(x.crosvm, y.crosvm) }
+    }
+}
+
+// Get guest time from /proc/[crosvm pid]/stat
+fn get_guest_time(pid: u32) -> Result<i64> {
+    let file = read_to_string(format!("/proc/{}/stat", pid))?;
+    let data_list: Vec<_> = file.split_whitespace().collect();
+
+    // Information about guest_time is at 43th place of the file split with the whitespace.
+    // Example of /proc/[pid]/stat :
+    // 6603 (kworker/104:1H-kblockd) I 2 0 0 0 -1 69238880 0 0 0 0 0 88 0 0 0 -20 1 0 1845 0 0
+    // 18446744073709551615 0 0 0 0 0 0 0 2147483647 0 0 0 0 17 104 0 0 0 0 0 0 0 0 0 0 0 0 0
+    if data_list.len() < 43 {
+        bail!("Failed to parse command result for getting guest time : {}", file);
+    }
+
+    let guest_time_ticks = data_list[42].parse::<i64>()?;
+    // SAFETY : It just returns an integer about CPU tick information.
+    let ticks_per_sec = unsafe { sysconf(_SC_CLK_TCK) } as i64;
+    Ok(guest_time_ticks * MILLIS_PER_SEC / ticks_per_sec)
+}
+
+// Get rss from /proc/[crosvm pid]/smaps
+fn get_rss(pid: u32) -> Result<Rss> {
+    let file = read_to_string(format!("/proc/{}/smaps", pid))?;
+    let lines: Vec<_> = file.split('\n').collect();
+
+    let mut rss_vm_total = 0i64;
+    let mut rss_crosvm_total = 0i64;
+    let mut is_vm = false;
+    for line in lines {
+        if line.contains("crosvm_guest") {
+            is_vm = true;
+        } else if line.contains("Rss:") {
+            let data_list: Vec<_> = line.split_whitespace().collect();
+            if data_list.len() < 2 {
+                bail!("Failed to parse command result for getting rss :\n{}", line);
+            }
+            let rss = data_list[1].parse::<i64>()?;
+
+            if is_vm {
+                rss_vm_total += rss;
+                is_vm = false;
+            }
+            rss_crosvm_total += rss;
+        }
+    }
+
+    Ok(Rss { vm: rss_vm_total, crosvm: rss_crosvm_total })
+}
+
 fn death_reason(result: &Result<ExitStatus, io::Error>, mut failure_reason: &str) -> DeathReason {
     if let Some(position) = failure_reason.find('|') {
         // Separator indicates extra context information is present after the failure name.
diff --git a/vm/src/run.rs b/vm/src/run.rs
index 53402da..1f0433d 100644
--- a/vm/src/run.rs
+++ b/vm/src/run.rs
@@ -27,7 +27,7 @@
 use binder::ParcelFileDescriptor;
 use microdroid_payload_config::VmPayloadConfig;
 use std::fs::File;
-use std::io::{self, BufRead, BufReader};
+use std::io;
 use std::os::unix::io::{AsRawFd, FromRawFd};
 use std::path::{Path, PathBuf};
 use vmclient::{ErrorCode, VmInstance};
@@ -276,19 +276,8 @@
 struct Callback {}
 
 impl vmclient::VmCallback for Callback {
-    fn on_payload_started(&self, _cid: i32, stream: Option<&File>) {
-        // Show the output of the payload
-        if let Some(stream) = stream {
-            let mut reader = BufReader::new(stream.try_clone().unwrap());
-            std::thread::spawn(move || loop {
-                let mut s = String::new();
-                match reader.read_line(&mut s) {
-                    Ok(0) => break,
-                    Ok(_) => print!("{}", s),
-                    Err(e) => eprintln!("error reading from virtual machine: {}", e),
-                };
-            });
-        }
+    fn on_payload_started(&self, _cid: i32) {
+        eprintln!("payload started");
     }
 
     fn on_payload_ready(&self, _cid: i32) {
diff --git a/vmbase/example/Android.bp b/vmbase/example/Android.bp
index e9a3f98..0f1e66a 100644
--- a/vmbase/example/Android.bp
+++ b/vmbase/example/Android.bp
@@ -54,10 +54,11 @@
     edition: "2021",
     rustlibs: [
         "android.system.virtualizationservice-rust",
+        "libandroid_logger",
         "libanyhow",
-        "libenv_logger",
         "liblibc",
         "liblog_rust",
+        "libnix",
         "libvmclient",
     ],
     data: [
diff --git a/vmbase/example/tests/test.rs b/vmbase/example/tests/test.rs
index 85e0213..57b68ed 100644
--- a/vmbase/example/tests/test.rs
+++ b/vmbase/example/tests/test.rs
@@ -25,8 +25,9 @@
 use log::info;
 use std::{
     fs::File,
-    io,
-    os::unix::io::{AsRawFd, FromRawFd},
+    io::{self, BufRead, BufReader},
+    os::unix::io::FromRawFd,
+    panic, thread,
 };
 use vmclient::{DeathReason, VmInstance};
 
@@ -36,7 +37,14 @@
 /// Runs the vmbase_example VM as an unprotected VM via VirtualizationService.
 #[test]
 fn test_run_example_vm() -> Result<(), Error> {
-    env_logger::init();
+    android_logger::init_once(
+        android_logger::Config::default().with_tag("vmbase").with_min_level(log::Level::Debug),
+    );
+
+    // Redirect panic messages to logcat.
+    panic::set_hook(Box::new(|panic_info| {
+        log::error!("{}", panic_info);
+    }));
 
     // We need to start the thread pool for Binder to work properly, especially link_to_death.
     ProcessState::start_thread_pool();
@@ -62,8 +70,8 @@
         platformVersion: "~1.0".to_string(),
         taskProfiles: vec![],
     });
-    let console = duplicate_stdout()?;
-    let log = duplicate_stdout()?;
+    let console = android_log_fd()?;
+    let log = android_log_fd()?;
     let vm = VmInstance::create(service.as_ref(), &config, Some(console), Some(log), None)
         .context("Failed to create VM")?;
     vm.start().context("Failed to start VM")?;
@@ -76,17 +84,17 @@
     Ok(())
 }
 
-/// Safely duplicate the standard output file descriptor.
-fn duplicate_stdout() -> io::Result<File> {
-    let stdout_fd = io::stdout().as_raw_fd();
-    // Safe because this just duplicates a file descriptor which we know to be valid, and we check
-    // for an error.
-    let dup_fd = unsafe { libc::dup(stdout_fd) };
-    if dup_fd < 0 {
-        Err(io::Error::last_os_error())
-    } else {
-        // Safe because we have just duplicated the file descriptor so we own it, and `from_raw_fd`
-        // takes ownership of it.
-        Ok(unsafe { File::from_raw_fd(dup_fd) })
-    }
+fn android_log_fd() -> io::Result<File> {
+    let (reader_fd, writer_fd) = nix::unistd::pipe()?;
+
+    // SAFETY: These are new FDs with no previous owner.
+    let reader = unsafe { File::from_raw_fd(reader_fd) };
+    let writer = unsafe { File::from_raw_fd(writer_fd) };
+
+    thread::spawn(|| {
+        for line in BufReader::new(reader).lines() {
+            info!("{}", line.unwrap());
+        }
+    });
+    Ok(writer)
 }
diff --git a/vmclient/src/lib.rs b/vmclient/src/lib.rs
index e6f32b4..20b7f02 100644
--- a/vmclient/src/lib.rs
+++ b/vmclient/src/lib.rs
@@ -74,7 +74,7 @@
 pub trait VmCallback {
     /// Called when the payload has been started within the VM. If present, `stream` is connected
     /// to the stdin/stdout of the payload.
-    fn on_payload_started(&self, cid: i32, stream: Option<&File>) {}
+    fn on_payload_started(&self, cid: i32) {}
 
     /// Callend when the payload has notified Virtualization Service that it is ready to serve
     /// clients.
@@ -269,14 +269,10 @@
 impl Interface for VirtualMachineCallback {}
 
 impl IVirtualMachineCallback for VirtualMachineCallback {
-    fn onPayloadStarted(
-        &self,
-        cid: i32,
-        stream: Option<&ParcelFileDescriptor>,
-    ) -> BinderResult<()> {
+    fn onPayloadStarted(&self, cid: i32) -> BinderResult<()> {
         self.state.notify_state(VirtualMachineState::STARTED);
         if let Some(ref callback) = self.client_callback {
-            callback.on_payload_started(cid, stream.map(ParcelFileDescriptor::as_ref));
+            callback.on_payload_started(cid);
         }
         Ok(())
     }