Merge "Collect cpu/mem stats via VmExited atom"
diff --git a/apex/Android.bp b/apex/Android.bp
index e0ca9bf..2d6c757 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -15,20 +15,55 @@
     "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",
+    ],
+}
+
+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 +87,12 @@
         "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/compos/common/compos_client.rs b/compos/common/compos_client.rs
index 68e1948..02459b2 100644
--- a/compos/common/compos_client.rs
+++ b/compos/common/compos_client.rs
@@ -240,15 +240,17 @@
 
 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);
     }
 
+    fn on_payload_stdio(&self, cid: i32, stream: &File) {
+        if let Err(e) = start_logging(stream) {
+            log::warn!("Can't log vm output: {}", e);
+        };
+        log::info!("VM payload forwarded its stdio, cid = {}", cid);
+    }
+
     fn on_payload_ready(&self, cid: i32) {
         log::info!("VM payload ready, cid = {}", cid);
     }
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..0e8b9f5 100644
--- a/compos/src/compsvc.rs
+++ b/compos/src/compsvc.rs
@@ -144,7 +144,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/compos/src/compsvc_main.rs b/compos/src/compsvc_main.rs
index a4e3903..c280956 100644
--- a/compos/src/compsvc_main.rs
+++ b/compos/src/compsvc_main.rs
@@ -24,10 +24,10 @@
 
 use anyhow::{bail, Result};
 use compos_common::COMPOS_VSOCK_PORT;
-use log::{debug, error};
+use log::{debug, error, warn};
 use rpcbinder::run_vsock_rpc_server;
 use std::panic;
-use vm_payload_bindgen::AVmPayload_notifyPayloadReady;
+use vm_payload_bindgen::{AVmPayload_notifyPayloadReady, AVmPayload_setupStdioProxy};
 
 fn main() {
     if let Err(e) = try_main() {
@@ -44,6 +44,10 @@
     panic::set_hook(Box::new(|panic_info| {
         error!("{}", panic_info);
     }));
+    // Redirect stdio to the host.
+    if !unsafe { AVmPayload_setupStdioProxy() } {
+        warn!("Failed to setup stdio proxy");
+    }
 
     let service = compsvc::new_binder()?.as_binder();
     debug!("compsvc is starting as a rpc service.");
diff --git a/demo/Android.bp b/demo/Android.bp
index 8613166..5241e25 100644
--- a/demo/Android.bp
+++ b/demo/Android.bp
@@ -13,7 +13,7 @@
         "com.google.android.material_material",
     ],
     libs: [
-        "android.system.virtualmachine",
+        "framework-virtualization",
     ],
     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..ebc2bb3 100644
--- a/demo/java/com/android/microdroid/demo/MainActivity.java
+++ b/demo/java/com/android/microdroid/demo/MainActivity.java
@@ -169,13 +169,11 @@
                         private final ExecutorService mService = mExecutorService;
 
                         @Override
-                        public void onPayloadStarted(VirtualMachine vm,
-                                ParcelFileDescriptor stream) {
-                            if (stream == null) {
-                                mPayloadOutput.postValue("(no output available)");
-                                return;
-                            }
+                        public void onPayloadStarted(VirtualMachine vm) {}
 
+                        @Override
+                        public void onPayloadStdio(VirtualMachine vm, ParcelFileDescriptor stream) {
+                            mPayloadOutput.postValue("(Payload connected standard output...)");
                             InputStream input = new FileInputStream(stream.getFileDescriptor());
                             mService.execute(new Reader("payload", mPayloadOutput, input));
                         }
diff --git a/docs/getting_started/index.md b/docs/getting_started/index.md
index 245aba6..5f552f9 100644
--- a/docs/getting_started/index.md
+++ b/docs/getting_started/index.md
@@ -133,7 +133,8 @@
   --debug full \
   /data/local/tmp/virt/MicrodroidDemoApp.apk \
   /data/local/tmp/virt/MicrodroidDemoApp.apk.idsig \
-  /data/local/tmp/virt/instance.img assets/vm_config.json
+  /data/local/tmp/virt/instance.img \
+  --payload-path MicrodroidTestNativeLib.so
 ```
 
 ## Building and updating CrosVM and VirtualizationService {#building-and-updating}
diff --git a/javalib/Android.bp b/javalib/Android.bp
index cb03fa1..04ed273 100644
--- a/javalib/Android.bp
+++ b/javalib/Android.bp
@@ -11,10 +11,20 @@
 }
 
 java_sdk_library {
-    name: "android.system.virtualmachine",
-    installable: true,
+    name: "framework-virtualization",
+    installable: false,
     compile_dex: true,
 
+    // TODO(b/243512044): introduce non-updatable-framework-module-defaults
+
+    defaults: ["framework-module-defaults"],
+
+    shared_library: false,
+
+    default_to_stubs: false,
+
+    dist_group: "android",
+
     jarjar_rules: "jarjar-rules.txt",
 
     srcs: ["src/**/*.java"],
@@ -25,6 +35,7 @@
     ],
 
     apex_available: ["com.android.virt"],
+
     permitted_packages: [
         "android.system.virtualmachine",
         "android.system.virtualizationservice",
@@ -38,6 +49,38 @@
             "-Xep:GuardedBy:ERROR",
         ],
     },
+
+    public: {
+        enabled: true,
+        sdk_version: "module_current",
+    },
+
+    system: {
+        enabled: true,
+        sdk_version: "module_current",
+    },
+
+    module_lib: {
+        enabled: true,
+        sdk_version: "module_current",
+    },
+
+    test: {
+        enabled: true,
+        sdk_version: "module_current",
+    },
+
+    sdk_version: "core_platform",
+    platform_apis: true,
+    impl_only_libs: [
+        "framework",
+    ],
+    impl_library_visibility: [
+        "//frameworks/base",
+    ],
+
+    // Temporary workaround, will be removed in a follow-up child cl.
+    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 7c826b6..c200d00 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;
@@ -76,6 +77,7 @@
 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;
@@ -285,10 +287,45 @@
         return sInstances.computeIfAbsent(context, unused -> new HashMap<>());
     }
 
+    /**
+     * 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("sCreateLock")
     @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());
+            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
+            try {
+                deleteRecursively(vmDir);
+            } catch (IOException innerException) {
+                e.addSuppressed(innerException);
+            }
+            throw e;
+        }
     }
 
     /**
@@ -301,31 +338,11 @@
     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) {
@@ -374,13 +391,7 @@
             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);
-        }
-
+        VirtualMachineConfig config = VirtualMachineConfig.from(configFilePath);
         Map<String, WeakReference<VirtualMachine>> instancesMap = getInstancesMap(context);
 
         VirtualMachine vm = null;
@@ -425,6 +436,33 @@
         if (instancesMap != null) instancesMap.remove(name);
     }
 
+    @GuardedBy("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);
+    }
+
     /**
      * Returns the name of this virtual machine. The name is unique in the package and can't be
      * changed.
@@ -643,9 +681,14 @@
                 mVirtualMachine.registerCallback(
                         new IVirtualMachineCallback.Stub() {
                             @Override
-                            public void onPayloadStarted(int cid, ParcelFileDescriptor stream) {
+                            public void onPayloadStarted(int cid) {
+                                executeCallback((cb) -> cb.onPayloadStarted(VirtualMachine.this));
+                            }
+
+                            @Override
+                            public void onPayloadStdio(int cid, ParcelFileDescriptor stream) {
                                 executeCallback(
-                                        (cb) -> cb.onPayloadStarted(VirtualMachine.this, stream));
+                                        (cb) -> cb.onPayloadStdio(VirtualMachine.this, stream));
                             }
 
                             @Override
@@ -656,16 +699,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 +721,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 +884,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;
         }
@@ -895,22 +934,23 @@
     }
 
     /**
-     * Captures the current state of the VM in a {@link ParcelVirtualMachine} instance.
-     * The VM needs to be stopped to avoid inconsistency in its state representation.
+     * Captures the current state of the VM in a {@link VirtualMachineDescriptor} instance. The VM
+     * needs to be stopped to avoid inconsistency in its state representation.
      *
-     * @return a {@link ParcelVirtualMachine} instance that represents the VM's state.
+     * @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 ParcelVirtualMachine toParcelVirtualMachine() throws VirtualMachineException {
+    public VirtualMachineDescriptor toDescriptor() throws VirtualMachineException {
         synchronized (mLock) {
             checkStopped();
         }
         try {
-            return new ParcelVirtualMachine(
-                ParcelFileDescriptor.open(mConfigFilePath, MODE_READ_ONLY),
-                ParcelFileDescriptor.open(mInstanceFilePath, MODE_READ_ONLY));
+            return new VirtualMachineDescriptor(
+                    ParcelFileDescriptor.open(mConfigFilePath, MODE_READ_ONLY),
+                    ParcelFileDescriptor.open(mInstanceFilePath, MODE_READ_ONLY));
         } catch (IOException e) {
             throw new VirtualMachineException(e);
         }
@@ -1065,4 +1105,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..26b8ba2 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,11 @@
     /** 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 creates a standard input/output stream. */
+    void onPayloadStdio(@NonNull VirtualMachine vm, @NonNull ParcelFileDescriptor stream);
 
     /**
      * 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/ParcelVirtualMachine.java b/javalib/src/android/system/virtualmachine/VirtualMachineDescriptor.java
similarity index 61%
rename from javalib/src/android/system/virtualmachine/ParcelVirtualMachine.java
rename to javalib/src/android/system/virtualmachine/VirtualMachineDescriptor.java
index 808f30a..b51cbce 100644
--- a/javalib/src/android/system/virtualmachine/ParcelVirtualMachine.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineDescriptor.java
@@ -23,20 +23,18 @@
 import android.os.ParcelFileDescriptor;
 import android.os.Parcelable;
 
-import com.android.internal.annotations.VisibleForTesting;
-
 /**
- * A parcelable that captures the state of a Virtual Machine.
+ * A VM descriptor that captures the state of a Virtual Machine.
  *
  * <p>You can capture the current state of VM by creating an instance of this class with {@link
- * VirtualMachine#toParcelVirtualMachine()}, optionally pass it to another App, and then build an
- * identical VM with the parcel received.
+ * VirtualMachine#toDescriptor()}, optionally pass it to another App, and then build an identical VM
+ * with the descriptor received.
  *
  * @hide
  */
-public final class ParcelVirtualMachine implements Parcelable {
-    private final @NonNull ParcelFileDescriptor mConfigFd;
-    private final @NonNull ParcelFileDescriptor mInstanceImgFd;
+public final class VirtualMachineDescriptor implements Parcelable {
+    @NonNull private final ParcelFileDescriptor mConfigFd;
+    @NonNull private final ParcelFileDescriptor mInstanceImgFd;
     // TODO(b/243129654): Add trusted storage fd once it is available.
 
     @Override
@@ -45,47 +43,46 @@
     }
 
     @Override
-    public void writeToParcel(Parcel out, int flags) {
+    public void writeToParcel(@NonNull Parcel out, int flags) {
         mConfigFd.writeToParcel(out, flags);
         mInstanceImgFd.writeToParcel(out, flags);
     }
 
-    public static final Parcelable.Creator<ParcelVirtualMachine> CREATOR =
-            new Parcelable.Creator<ParcelVirtualMachine>() {
-                public ParcelVirtualMachine createFromParcel(Parcel in) {
-                    return new ParcelVirtualMachine(in);
+    @NonNull
+    public static final Parcelable.Creator<VirtualMachineDescriptor> CREATOR =
+            new Parcelable.Creator<>() {
+                public VirtualMachineDescriptor createFromParcel(Parcel in) {
+                    return new VirtualMachineDescriptor(in);
                 }
 
-                public ParcelVirtualMachine[] newArray(int size) {
-                    return new ParcelVirtualMachine[size];
+                public VirtualMachineDescriptor[] newArray(int size) {
+                    return new VirtualMachineDescriptor[size];
                 }
             };
 
     /**
      * @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;
     }
 
-    ParcelVirtualMachine(
+    VirtualMachineDescriptor(
             @NonNull ParcelFileDescriptor configFd, @NonNull ParcelFileDescriptor instanceImgFd) {
         mConfigFd = configFd;
         mInstanceImgFd = instanceImgFd;
     }
 
-    private ParcelVirtualMachine(Parcel in) {
+    private VirtualMachineDescriptor(Parcel in) {
         mConfigFd = requireNonNull(in.readFileDescriptor());
         mInstanceImgFd = requireNonNull(in.readFileDescriptor());
     }
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineManager.java b/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
index 34b9fd9..c357f50 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
@@ -142,6 +142,24 @@
     }
 
     /**
+     * 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 (VirtualMachine.sCreateLock) {
+            return VirtualMachine.fromDescriptor(mContext, name, vmDescriptor);
+        }
+    }
+
+    /**
      * Returns an existing {@link VirtualMachine} with the given name. Returns null if there is no
      * such virtual machine.
      *
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/README.md b/microdroid/README.md
index 2519416..41278a5 100644
--- a/microdroid/README.md
+++ b/microdroid/README.md
@@ -141,7 +141,7 @@
 PATH_TO_YOUR_APP \
 $TEST_ROOT/MyApp.apk.idsig \
 $TEST_ROOT/instance.img \
-assets/VM_CONFIG_FILE
+--config-path assets/VM_CONFIG_FILE
 ```
 
 The last command lets you know the CID assigned to the VM. The console output
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..d5853a1 100644
--- a/microdroid/vm_payload/include/vm_payload.h
+++ b/microdroid/vm_payload/include/vm_payload.h
@@ -80,4 +80,13 @@
  */
 const char *AVmPayload_getApkContentsPath(void);
 
+/**
+ * Initiates a socket connection with the host and duplicates stdin, stdout and
+ * stderr file descriptors to the socket.
+ *
+ * \return true on success and false on failure. If unsuccessful, the stdio FDs
+ * may be in an inconsistent state.
+ */
+bool AVmPayload_setupStdioProxy();
+
 __END_DECLS
diff --git a/microdroid/vm_payload/src/lib.rs b/microdroid/vm_payload/src/lib.rs
index be6cf93..65b59bf 100644
--- a/microdroid/vm_payload/src/lib.rs
+++ b/microdroid/vm_payload/src/lib.rs
@@ -18,5 +18,5 @@
 
 pub use vm_payload_service::{
     AVmPayload_getDiceAttestationCdi, AVmPayload_getDiceAttestationChain,
-    AVmPayload_getVmInstanceSecret, AVmPayload_notifyPayloadReady,
+    AVmPayload_getVmInstanceSecret, AVmPayload_notifyPayloadReady, AVmPayload_setupStdioProxy,
 };
diff --git a/microdroid/vm_payload/src/vm_payload_service.rs b/microdroid/vm_payload/src/vm_payload_service.rs
index 098d246..e89f730 100644
--- a/microdroid/vm_payload/src/vm_payload_service.rs
+++ b/microdroid/vm_payload/src/vm_payload_service.rs
@@ -21,8 +21,11 @@
 use lazy_static::lazy_static;
 use log::{error, info, Level};
 use rpcbinder::{get_unix_domain_rpc_interface, run_vsock_rpc_server};
+use std::io;
 use std::ffi::CString;
+use std::fs::File;
 use std::os::raw::{c_char, c_void};
+use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd};
 
 lazy_static! {
     static ref VM_APK_CONTENTS_PATH_C: CString =
@@ -202,6 +205,36 @@
     get_vm_payload_service()?.getDiceAttestationCdi().context("Cannot get attestation CDI")
 }
 
+/// Creates a socket connection with the host and duplicates standard I/O
+/// file descriptors of the payload to that socket. Then notifies the host.
+#[no_mangle]
+pub extern "C" fn AVmPayload_setupStdioProxy() -> bool {
+    if let Err(e) = try_setup_stdio_proxy() {
+        error!("{:?}", e);
+        false
+    } else {
+        info!("Successfully set up stdio proxy to the host");
+        true
+    }
+}
+
+fn dup2(old_fd: &File, new_fd: BorrowedFd) -> Result<(), io::Error> {
+    // SAFETY - ownership does not change, only modifies the underlying raw FDs.
+    match unsafe { libc::dup2(old_fd.as_raw_fd(), new_fd.as_raw_fd()) } {
+        -1 => Err(io::Error::last_os_error()),
+        _ => Ok(()),
+    }
+}
+
+fn try_setup_stdio_proxy() -> Result<()> {
+    let fd =
+        get_vm_payload_service()?.setupStdioProxy().context("Could not connect a host socket")?;
+    dup2(fd.as_ref(), io::stdin().as_fd()).context("Failed to dup stdin")?;
+    dup2(fd.as_ref(), io::stdout().as_fd()).context("Failed to dup stdout")?;
+    dup2(fd.as_ref(), io::stderr().as_fd()).context("Failed to dup stderr")?;
+    Ok(())
+}
+
 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))
diff --git a/microdroid_manager/aidl/android/system/virtualization/payload/IVmPayloadService.aidl b/microdroid_manager/aidl/android/system/virtualization/payload/IVmPayloadService.aidl
index f8e7d34..1141965 100644
--- a/microdroid_manager/aidl/android/system/virtualization/payload/IVmPayloadService.aidl
+++ b/microdroid_manager/aidl/android/system/virtualization/payload/IVmPayloadService.aidl
@@ -16,6 +16,8 @@
 
 package android.system.virtualization.payload;
 
+import android.os.ParcelFileDescriptor;
+
 /**
  * This interface regroups the tasks that payloads delegate to
  * Microdroid Manager for execution.
@@ -61,4 +63,16 @@
      * @throws SecurityException if the use of test APIs is not permitted.
      */
     byte[] getDiceAttestationCdi();
+
+    /**
+     * Sets up a standard I/O proxy to the host.
+     *
+     * Creates a socket with the host and notifies its listeners that the stdio
+     * proxy is ready.
+     *
+     * Temporarily uses a random free port allocated by the OS.
+     * @return a file descriptor that the payload should dup() its standard I/O
+     * file descriptors to.
+     */
+    ParcelFileDescriptor setupStdioProxy();
 }
diff --git a/microdroid_manager/src/main.rs b/microdroid_manager/src/main.rs
index c18dd26..762a149 100644
--- a/microdroid_manager/src/main.rs
+++ b/microdroid_manager/src/main.rs
@@ -26,7 +26,7 @@
 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 anyhow::{anyhow, bail, ensure, Context, Error, Result};
@@ -49,15 +49,13 @@
 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";
@@ -732,7 +730,14 @@
 /// virtualizationservice in the host side.
 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
+        }
+    };
 
     info!("notifying payload started");
     service.notifyPayloadStarted()?;
@@ -751,40 +756,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/microdroid_manager/src/vm_payload_service.rs b/microdroid_manager/src/vm_payload_service.rs
index fcfc79d..249a2d8 100644
--- a/microdroid_manager/src/vm_payload_service.rs
+++ b/microdroid_manager/src/vm_payload_service.rs
@@ -18,15 +18,18 @@
 use android_system_virtualization_payload::aidl::android::system::virtualization::payload::IVmPayloadService::{
     BnVmPayloadService, IVmPayloadService, VM_PAYLOAD_SERVICE_SOCKET_NAME};
 use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice::IVirtualMachineService::IVirtualMachineService;
-use anyhow::{bail, Result};
-use binder::{Interface, BinderFeatures, ExceptionCode, Status, Strong};
+use anyhow::{bail, Context, Result};
+use binder::{Interface, BinderFeatures, ExceptionCode, ParcelFileDescriptor, Status, Strong};
 use log::{error, info};
 use openssl::hkdf::hkdf;
 use openssl::md::Md;
 use rpcbinder::run_init_unix_domain_rpc_server;
+use std::fs::File;
 use std::sync::mpsc;
 use std::thread;
 use std::time::Duration;
+use std::os::unix::io::{FromRawFd, IntoRawFd};
+use vsock::VsockListener;
 
 /// Implementation of `IVmPayloadService`.
 struct VmPayloadService {
@@ -67,6 +70,16 @@
         self.check_restricted_apis_allowed()?;
         Ok(self.dice.cdi_attest.to_vec())
     }
+
+    fn setupStdioProxy(&self) -> binder::Result<ParcelFileDescriptor> {
+        let f = self.setup_payload_stdio_proxy().map_err(|e| {
+            Status::new_service_specific_error_str(
+                -1,
+                Some(format!("Failed to create stdio proxy: {:?}", e)),
+            )
+        })?;
+        Ok(ParcelFileDescriptor::new(f))
+    }
 }
 
 impl Interface for VmPayloadService {}
@@ -89,6 +102,22 @@
             Err(Status::new_exception_str(ExceptionCode::SECURITY, Some("Use of restricted APIs")))
         }
     }
+
+    fn setup_payload_stdio_proxy(&self) -> Result<File> {
+        // Instead of a predefined port in the host, we open up a port in the guest and have
+        // the host connect to it. This makes it possible to have per-app instances of VS.
+        const ANY_PORT: u32 = 0;
+        let listener = VsockListener::bind_with_cid_port(libc::VMADDR_CID_HOST, ANY_PORT)
+            .context("Failed to create vsock listener")?;
+        let addr = listener.local_addr().context("Failed to resolve listener port")?;
+        self.virtual_machine_service
+            .connectPayloadStdioProxy(addr.port() as i32)
+            .context("Failed to connect to the host")?;
+        let (stream, _) =
+            listener.accept().context("Failed to accept vsock connection from the host")?;
+        // SAFETY: ownership is transferred from stream to the new File
+        Ok(unsafe { File::from_raw_fd(stream.into_raw_fd()) })
+    }
 }
 
 /// Registers the `IVmPayloadService` service.
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/benchmark/Android.bp b/tests/benchmark/Android.bp
index e6f39f8..bccea6b 100644
--- a/tests/benchmark/Android.bp
+++ b/tests/benchmark/Android.bp
@@ -16,7 +16,7 @@
         "com.android.microdroid.testservice-java",
         "truth-prebuilt",
     ],
-    libs: ["android.system.virtualmachine"],
+    libs: ["framework-virtualization"],
     jni_libs: [
         "MicrodroidBenchmarkNativeLib",
         "MicrodroidIdleNativeLib",
@@ -28,20 +28,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 +41,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..bd92020 100644
--- a/tests/helper/Android.bp
+++ b/tests/helper/Android.bp
@@ -24,5 +24,5 @@
         "VirtualizationTestHelper",
         "truth-prebuilt",
     ],
-    libs: ["android.system.virtualmachine"],
+    libs: ["framework-virtualization"],
 }
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..d1e1f6c 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;
@@ -119,17 +119,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 +227,10 @@
         }
 
         @Override
-        public void onPayloadStarted(VirtualMachine vm, ParcelFileDescriptor stream) {}
+        public void onPayloadStarted(VirtualMachine vm) {}
+
+        @Override
+        public void onPayloadStdio(VirtualMachine vm, ParcelFileDescriptor stream) {}
 
         @Override
         public void onPayloadReady(VirtualMachine vm) {}
@@ -327,7 +325,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/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java b/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java
index 43fe615..9bcd1d3 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
@@ -100,6 +100,9 @@
     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());
     }
 
diff --git a/tests/testapk/Android.bp b/tests/testapk/Android.bp
index 8972046..8d49721 100644
--- a/tests/testapk/Android.bp
+++ b/tests/testapk/Android.bp
@@ -19,7 +19,7 @@
         "truth-prebuilt",
         "compatibility-common-util-devicesidelib",
     ],
-    libs: ["android.system.virtualmachine"],
+    libs: ["framework-virtualization"],
     jni_libs: [
         "MicrodroidTestNativeLib",
         "MicrodroidIdleNativeLib",
@@ -55,3 +55,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 cc623a8..5e86798 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -30,10 +30,10 @@
 import android.os.ParcelFileDescriptor;
 import android.os.ServiceSpecificException;
 import android.os.SystemProperties;
-import android.system.virtualmachine.ParcelVirtualMachine;
 import android.system.virtualmachine.VirtualMachine;
 import android.system.virtualmachine.VirtualMachineCallback;
 import android.system.virtualmachine.VirtualMachineConfig;
+import android.system.virtualmachine.VirtualMachineDescriptor;
 import android.system.virtualmachine.VirtualMachineException;
 import android.system.virtualmachine.VirtualMachineManager;
 import android.util.Log;
@@ -61,6 +61,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;
@@ -600,31 +601,46 @@
     }
 
     @Test
-    public void vmConvertsToValidParcelVm() throws Exception {
+    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", 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
-        ParcelVirtualMachine parcelVm = vm.toParcelVirtualMachine();
+        VirtualMachine vmImport = vmm.importFromDescriptor(vmNameImport, descriptor);
 
         // Asserts
-        assertFileContentsAreEqual(parcelVm.getConfigFd(), vmName, "config.xml");
-        assertFileContentsAreEqual(parcelVm.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();
         }
     }
 
@@ -671,8 +687,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");
@@ -695,11 +712,16 @@
                     }
 
                     @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");
+                    }
+
+                    @Override
+                    public void onPayloadStdio(VirtualMachine vm, ParcelFileDescriptor stream) {
+                        Log.i(TAG, "onPayloadStdio");
+                        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..1b18ce9 100644
--- a/tests/testapk/src/native/testbinary.cpp
+++ b/tests/testapk/src/native/testbinary.cpp
@@ -158,6 +158,9 @@
 } // Anonymous namespace
 
 extern "C" int AVmPayload_main() {
+    // Forward standard I/O to the host.
+    AVmPayload_setupStdioProxy();
+
     // disable buffering to communicate seamlessly
     setvbuf(stdin, nullptr, _IONBF, 0);
     setvbuf(stdout, nullptr, _IONBF, 0);
diff --git a/virtualizationservice/aidl/Android.bp b/virtualizationservice/aidl/Android.bp
index 4d5326a..da237f8 100644
--- a/virtualizationservice/aidl/Android.bp
+++ b/virtualizationservice/aidl/Android.bp
@@ -39,6 +39,9 @@
     imports: ["android.system.virtualizationcommon"],
     unstable: true,
     backend: {
+        java: {
+            sdk_version: "module_current",
+        },
         rust: {
             enabled: true,
             apex_available: [
@@ -55,6 +58,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..521cf12 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachineCallback.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachineCallback.aidl
@@ -24,13 +24,14 @@
  */
 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 provides access to its standard input/output via a socket.
+     */
+    void onPayloadStdio(int cid, in ParcelFileDescriptor fd);
 
     /**
      * Called when the payload in the VM is ready to serve.
diff --git a/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl b/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl
index e8c1724..deee662 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,12 @@
     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);
+
+    /**
+     * Notifies that the guest has started a stdio proxy on the given port.
+     */
+    void connectPayloadStdioProxy(int port);
 }
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index 00a46bf..30b89da 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -38,7 +38,7 @@
 };
 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};
@@ -301,12 +301,6 @@
     pub fn init() -> VirtualizationService {
         let service = VirtualizationService::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);
@@ -488,33 +482,6 @@
     }
 }
 
-/// 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 +500,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 +820,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);
             }
         }
@@ -894,6 +859,16 @@
         }
     }
 
+    /// Call all registered callbacks to notify that the payload has provided a standard I/O proxy.
+    pub fn notify_payload_stdio(&self, cid: Cid, fd: ParcelFileDescriptor) {
+        let callbacks = &*self.0.lock().unwrap();
+        for callback in callbacks {
+            if let Err(e) = callback.onPayloadStdio(cid as i32, &fd) {
+                error!("Error notifying payload stdio event from VM CID {}: {:?}", cid, e);
+            }
+        }
+    }
+
     /// Call all registered callbacks to say that the VM has died.
     pub fn callback_on_died(&self, cid: Cid, reason: DeathReason) {
         let callbacks = &*self.0.lock().unwrap();
@@ -1072,8 +1047,7 @@
             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_metric.lock().unwrap().start_timestamp;
             write_vm_booted_stats(vm.requester_uid as i32, &vm.name, vm_start_timestamp);
@@ -1140,6 +1114,27 @@
             ))
         }
     }
+
+    fn connectPayloadStdioProxy(&self, port: i32) -> binder::Result<()> {
+        let cid = self.cid;
+        if let Some(vm) = self.state.lock().unwrap().get_vm(cid) {
+            info!("VM with CID {} started a stdio proxy", cid);
+            let stream = VsockStream::connect_with_cid_port(cid, port as u32).map_err(|e| {
+                Status::new_service_specific_error_str(
+                    -1,
+                    Some(format!("Failed to connect to guest stdio proxy: {:?}", e)),
+                )
+            })?;
+            vm.callbacks.notify_payload_stdio(cid, vsock_stream_to_pfd(stream));
+            Ok(())
+        } else {
+            error!("connectPayloadStdioProxy is called from an unknown CID {}", cid);
+            Err(Status::new_service_specific_error_str(
+                -1,
+                Some(format!("cannot find a VM with CID {}", cid)),
+            ))
+        }
+    }
 }
 
 impl VirtualMachineService {
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 2ada1ec..db6da43 100644
--- a/virtualizationservice/src/crosvm.rs
+++ b/virtualizationservice/src/crosvm.rs
@@ -37,7 +37,6 @@
 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 binder::Strong;
 use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice::IVirtualMachineService::IVirtualMachineService;
@@ -218,8 +217,6 @@
     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 metrics of VM such as timestamp or cpu / memory usage.
@@ -251,7 +248,6 @@
             requester_uid,
             requester_debug_pid,
             callbacks: Default::default(),
-            stream: Mutex::new(None),
             vm_service: Mutex::new(None),
             vm_metric: Mutex::new(Default::default()),
             payload_state: Mutex::new(PayloadState::Starting),
diff --git a/vm/src/main.rs b/vm/src/main.rs
index 3b887d3..89d56d4 100644
--- a/vm/src/main.rs
+++ b/vm/src/main.rs
@@ -48,8 +48,13 @@
         instance: PathBuf,
 
         /// Path to VM config JSON within APK (e.g. assets/vm_config.json)
+        #[clap(long)]
         config_path: Option<String>,
 
+        /// Path to VM payload binary within APK (e.g. MicrodroidTestNativeLib.so)
+        #[clap(long)]
+        payload_path: Option<String>,
+
         /// Name of VM
         #[clap(long)]
         name: Option<String>,
@@ -201,6 +206,7 @@
             storage,
             storage_size,
             config_path,
+            payload_path,
             daemonize,
             console,
             log,
@@ -219,7 +225,8 @@
             &instance,
             storage.as_deref(),
             storage_size,
-            config_path.as_deref().unwrap_or(""),
+            config_path,
+            payload_path,
             daemonize,
             console.as_deref(),
             log.as_deref(),
diff --git a/vm/src/run.rs b/vm/src/run.rs
index de8f1c0..7cd5a19 100644
--- a/vm/src/run.rs
+++ b/vm/src/run.rs
@@ -20,6 +20,7 @@
     PartitionType::PartitionType,
     VirtualMachineAppConfig::{DebugLevel::DebugLevel, Payload::Payload, VirtualMachineAppConfig},
     VirtualMachineConfig::VirtualMachineConfig,
+    VirtualMachinePayloadConfig::VirtualMachinePayloadConfig,
     VirtualMachineState::VirtualMachineState,
 };
 use anyhow::{bail, Context, Error};
@@ -43,7 +44,8 @@
     instance: &Path,
     storage: Option<&Path>,
     storage_size: Option<u64>,
-    config_path: &str,
+    config_path: Option<String>,
+    payload_path: Option<String>,
     daemonize: bool,
     console_path: Option<&Path>,
     log_path: Option<&Path>,
@@ -57,7 +59,11 @@
 ) -> Result<(), Error> {
     let apk_file = File::open(apk).context("Failed to open APK file")?;
 
-    let extra_apks = parse_extra_apk_list(apk, config_path)?;
+    let extra_apks = match config_path.as_deref() {
+        Some(path) => parse_extra_apk_list(apk, path)?,
+        None => vec![],
+    };
+
     if extra_apks.len() != extra_idsigs.len() {
         bail!(
             "Found {} extra apks, but there are {} extra idsigs",
@@ -108,6 +114,19 @@
     let extra_idsig_files: Result<Vec<File>, _> = extra_idsigs.iter().map(File::open).collect();
     let extra_idsig_fds = extra_idsig_files?.into_iter().map(ParcelFileDescriptor::new).collect();
 
+    let payload = if let Some(config_path) = config_path {
+        if payload_path.is_some() {
+            bail!("Only one of --config-path or --payload-path can be defined")
+        }
+        Payload::ConfigPath(config_path)
+    } else if let Some(payload_path) = payload_path {
+        Payload::PayloadConfig(VirtualMachinePayloadConfig { payloadPath: payload_path })
+    } else {
+        bail!("Either --config-path or --payload-path must be defined")
+    };
+
+    let payload_config_str = format!("{:?}!{:?}", apk, payload);
+
     let config = VirtualMachineConfig::AppConfig(VirtualMachineAppConfig {
         name: name.unwrap_or_else(|| String::from("VmRunApp")),
         apk: apk_fd.into(),
@@ -115,22 +134,14 @@
         extraIdsigs: extra_idsig_fds,
         instanceImage: open_parcel_file(instance, true /* writable */)?.into(),
         encryptedStorageImage: storage,
-        payload: Payload::ConfigPath(config_path.to_owned()),
+        payload,
         debugLevel: debug_level,
         protectedVm: protected,
         memoryMib: mem.unwrap_or(0) as i32, // 0 means use the VM default
         numCpus: cpus.unwrap_or(1) as i32,
         taskProfiles: task_profiles,
     });
-    run(
-        service,
-        &config,
-        &format!("{:?}!{:?}", apk, config_path),
-        daemonize,
-        console_path,
-        log_path,
-        ramdump_path,
-    )
+    run(service, &config, &payload_config_str, daemonize, console_path, log_path, ramdump_path)
 }
 
 /// Run a VM from the given configuration file.
@@ -187,7 +198,7 @@
 fn run(
     service: &dyn IVirtualizationService,
     config: &VirtualMachineConfig,
-    config_path: &str,
+    payload_config: &str,
     daemonize: bool,
     console_path: Option<&Path>,
     log_path: Option<&Path>,
@@ -221,7 +232,7 @@
 
     println!(
         "Created VM from {} with CID {}, state is {}.",
-        config_path,
+        payload_config,
         vm.cid(),
         state_to_str(vm.state()?)
     );
@@ -265,19 +276,22 @@
 struct Callback {}
 
 impl vmclient::VmCallback for Callback {
-    fn on_payload_started(&self, _cid: i32, stream: Option<&File>) {
+    fn on_payload_started(&self, _cid: i32) {
+        eprintln!("payload started");
+    }
+
+    fn on_payload_stdio(&self, _cid: i32, stream: &File) {
+        eprintln!("connecting payload stdio...");
         // 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),
-                };
-            });
-        }
+        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_ready(&self, _cid: i32) {
diff --git a/vmclient/src/lib.rs b/vmclient/src/lib.rs
index e6f32b4..1dd553c 100644
--- a/vmclient/src/lib.rs
+++ b/vmclient/src/lib.rs
@@ -74,12 +74,15 @@
 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.
     fn on_payload_ready(&self, cid: i32) {}
 
+    /// Called by the payload to forward its standard I/O streams to the host.
+    fn on_payload_stdio(&self, cid: i32, fd: &File);
+
     /// Called when the payload has exited in the VM. `exit_code` is the exit code of the payload
     /// process.
     fn on_payload_finished(&self, cid: i32, exit_code: i32) {}
@@ -269,14 +272,17 @@
 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(())
+    }
+
+    fn onPayloadStdio(&self, cid: i32, stream: &ParcelFileDescriptor) -> BinderResult<()> {
+        if let Some(ref callback) = self.client_callback {
+            callback.on_payload_stdio(cid, stream.as_ref());
         }
         Ok(())
     }