Merge "pvmfw: Expect an appended BCC"
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index a6b1f95..43c89d4 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -4,10 +4,15 @@
 bpfmt = true
 clang_format = true
 jsonlint = true
+google_java_format = true
 pylint3 = true
 rustfmt = true
 xmllint = true
 
+[Tool Paths]
+google-java-format = ${REPO_ROOT}/prebuilts/tools/common/google-java-format/google-java-format
+google-java-format-diff = ${REPO_ROOT}/prebuilts/tools/common/google-java-format/google-java-format-diff.py
+
 [Builtin Hooks Options]
 clang_format = --commit ${PREUPLOAD_COMMIT} --style file --extensions c,h,cc,cpp
 rustfmt = --config-path=rustfmt.toml
diff --git a/authfs/tests/benchmarks/src/java/com/android/fs/benchmarks/AuthFsBenchmarks.java b/authfs/tests/benchmarks/src/java/com/android/fs/benchmarks/AuthFsBenchmarks.java
index 428c816..8cee496 100644
--- a/authfs/tests/benchmarks/src/java/com/android/fs/benchmarks/AuthFsBenchmarks.java
+++ b/authfs/tests/benchmarks/src/java/com/android/fs/benchmarks/AuthFsBenchmarks.java
@@ -20,6 +20,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assume.assumeFalse;
 import static org.junit.Assume.assumeTrue;
 
 import android.cts.host.utils.DeviceJUnit4ClassRunnerWithParameters;
@@ -28,11 +29,11 @@
 
 import com.android.fs.common.AuthFsTestRule;
 import com.android.microdroid.test.common.MetricsProcessor;
+import com.android.microdroid.test.host.MicrodroidHostTestCaseBase;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.metrics.proto.MetricMeasurement.DataType;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Measurements;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
-import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
 
 import org.junit.After;
 import org.junit.AfterClass;
@@ -52,7 +53,7 @@
 @RootPermissionTest
 @RunWith(DeviceJUnit4Parameterized.class)
 @UseParametersRunnerFactory(DeviceJUnit4ClassRunnerWithParameters.RunnerFactory.class)
-public class AuthFsBenchmarks extends BaseHostJUnit4Test {
+public class AuthFsBenchmarks extends MicrodroidHostTestCaseBase {
     private static final int TRIAL_COUNT = 5;
 
     /** Name of the measure_io binary on host. */
@@ -82,6 +83,7 @@
         AuthFsTestRule.setUpAndroid(getTestInformation());
         mAuthFsTestRule.setUpTest();
         assumeTrue(AuthFsTestRule.getDevice().supportsMicrodroid(mProtectedVm));
+        assumeFalse("Skip on CF; protected VM not supported", isCuttlefish());
         String metricsPrefix =
                 MetricsProcessor.getMetricPrefix(
                         getDevice().getProperty("debug.hypervisor.metrics_tag"));
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/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/java/com/android/microdroid/demo/MainActivity.java b/demo/java/com/android/microdroid/demo/MainActivity.java
index df6f44e..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));
                         }
@@ -278,8 +276,8 @@
                 mVirtualMachine.setCallback(Executors.newSingleThreadExecutor(), callback);
                 mStatus.postValue(mVirtualMachine.getStatus());
 
-                InputStream console = mVirtualMachine.getConsoleOutputStream();
-                InputStream log = mVirtualMachine.getLogOutputStream();
+                InputStream console = mVirtualMachine.getConsoleOutput();
+                InputStream log = mVirtualMachine.getLogOutput();
                 mExecutorService.execute(new Reader("console", mConsoleOutput, console));
                 mExecutorService.execute(new Reader("log", mLogOutput, log));
             } catch (VirtualMachineException e) {
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/src/android/system/virtualmachine/VirtualMachine.java b/javalib/src/android/system/virtualmachine/VirtualMachine.java
index 214e7e6..d1742b2 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachine.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachine.java
@@ -643,9 +643,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 +661,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 +683,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) {
@@ -722,7 +730,7 @@
      * @hide
      */
     @NonNull
-    public InputStream getConsoleOutputStream() throws VirtualMachineException {
+    public InputStream getConsoleOutput() throws VirtualMachineException {
         synchronized (mLock) {
             createVmPipes();
             return new FileInputStream(mConsoleReader.getFileDescriptor());
@@ -736,7 +744,7 @@
      * @hide
      */
     @NonNull
-    public InputStream getLogOutputStream() throws VirtualMachineException {
+    public InputStream getLogOutput() throws VirtualMachineException {
         synchronized (mLock) {
             createVmPipes();
             return new FileInputStream(mLogReader.getFileDescriptor());
@@ -895,22 +903,22 @@
     }
 
     /**
-     * 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.
      */
     @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);
         }
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 90b09c8..b814367 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -381,7 +381,7 @@
          * @hide
          */
         public Builder(@NonNull Context context) {
-            mContext = requireNonNull(context);
+            mContext = requireNonNull(context, "context must not be null");
             mDebugLevel = DEBUG_LEVEL_NONE;
             mNumCpus = 1;
         }
diff --git a/javalib/src/android/system/virtualmachine/ParcelVirtualMachine.java b/javalib/src/android/system/virtualmachine/VirtualMachineDescriptor.java
similarity index 74%
rename from javalib/src/android/system/virtualmachine/ParcelVirtualMachine.java
rename to javalib/src/android/system/virtualmachine/VirtualMachineDescriptor.java
index 808f30a..70532fc 100644
--- a/javalib/src/android/system/virtualmachine/ParcelVirtualMachine.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineDescriptor.java
@@ -26,15 +26,15 @@
 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 {
+public final class VirtualMachineDescriptor implements Parcelable {
     private final @NonNull ParcelFileDescriptor mConfigFd;
     private final @NonNull ParcelFileDescriptor mInstanceImgFd;
     // TODO(b/243129654): Add trusted storage fd once it is available.
@@ -50,14 +50,14 @@
         mInstanceImgFd.writeToParcel(out, flags);
     }
 
-    public static final Parcelable.Creator<ParcelVirtualMachine> CREATOR =
-            new Parcelable.Creator<ParcelVirtualMachine>() {
-                public ParcelVirtualMachine createFromParcel(Parcel in) {
-                    return new ParcelVirtualMachine(in);
+    public static final Parcelable.Creator<VirtualMachineDescriptor> CREATOR =
+            new Parcelable.Creator<VirtualMachineDescriptor>() {
+                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];
                 }
             };
 
@@ -79,13 +79,13 @@
         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/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/microdroid_manager.rc b/microdroid_manager/microdroid_manager.rc
index cfa70bd..c41ee38 100644
--- a/microdroid_manager/microdroid_manager.rc
+++ b/microdroid_manager/microdroid_manager.rc
@@ -1,6 +1,9 @@
 service microdroid_manager /system/bin/microdroid_manager
     disabled
+    # print android log to kmsg
     file /dev/kmsg w
+    # redirect stdout/stderr to kmsg_debug
+    stdio_to_kmsg
     setenv RUST_LOG info
     # TODO(jooyung) remove this when microdroid_manager becomes a daemon
     oneshot
diff --git a/microdroid_manager/src/main.rs b/microdroid_manager/src/main.rs
index 4b4f996..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};
@@ -39,6 +39,7 @@
 use log::{error, info};
 use microdroid_metadata::{write_metadata, Metadata, PayloadMetadata};
 use microdroid_payload_config::{OsConfig, Task, TaskType, VmPayloadConfig};
+use nix::sys::signal::Signal;
 use openssl::sha::Sha512;
 use payload::{get_apex_data_from_payload, load_metadata, to_metadata};
 use rand::Fill;
@@ -47,14 +48,14 @@
 use rustutils::system_properties::PropertyWatcher;
 use std::borrow::Cow::{Borrowed, Owned};
 use std::convert::TryInto;
-use std::fs::{self, create_dir, File, OpenOptions};
+use std::env;
+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";
@@ -152,6 +153,11 @@
 }
 
 fn main() -> Result<()> {
+    // If debuggable, print full backtrace to console log with stdio_to_kmsg
+    if system_properties::read_bool(APP_DEBUGGABLE_PROP, true)? {
+        env::set_var("RUST_BACKTRACE", "full");
+    }
+
     scopeguard::defer! {
         info!("Shutting down...");
         if let Err(e) = system_properties::write("sys.powerctl", "shutdown") {
@@ -724,16 +730,6 @@
 /// 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)?;
-
-    info!("notifying payload started");
-    service.notifyPayloadStarted()?;
-
-    let exit_status = command.spawn()?.wait()?;
-    exit_status.code().ok_or_else(|| anyhow!("Failed to get exit_code from the paylaod."))
-}
-
-fn build_command(task: &Task) -> Result<Command> {
     let mut command = match task.type_ {
         TaskType::Executable => Command::new(&task.command),
         TaskType::MicrodroidLauncher => {
@@ -743,28 +739,21 @@
         }
     };
 
-    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());
-        }
-    }
+    info!("notifying payload started");
+    service.notifyPayloadStarted()?;
 
-    Ok(command)
+    let exit_status = command.spawn()?.wait()?;
+    match exit_status.code() {
+        Some(exit_code) => Ok(exit_code),
+        None => Err(match exit_status.signal() {
+            Some(signal) => anyhow!(
+                "Payload exited due to signal: {} ({})",
+                signal,
+                Signal::try_from(signal).map_or("unknown", |s| s.as_str())
+            ),
+            None => anyhow!("Payload has neither exit code nor signal"),
+        }),
+    }
 }
 
 fn find_library_path(name: &str) -> Result<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/tests/benchmark/src/native/benchmarkbinary.cpp b/tests/benchmark/src/native/benchmarkbinary.cpp
index 6321c25..70c6884 100644
--- a/tests/benchmark/src/native/benchmarkbinary.cpp
+++ b/tests/benchmark/src/native/benchmarkbinary.cpp
@@ -96,7 +96,7 @@
         const int64_t block_count = fileSizeBytes / 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{}()};
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 ede838b..1e57ff8 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
@@ -202,8 +202,8 @@
                 throws VirtualMachineException, InterruptedException {
             vm.setCallback(mExecutorService, this);
             vm.run();
-            logVmOutputAndMonitorBootEvents(logTag, vm.getConsoleOutputStream(), "Console");
-            logVmOutput(logTag, vm.getLogOutputStream(), "Log");
+            logVmOutputAndMonitorBootEvents(logTag, vm.getConsoleOutput(), "Console");
+            logVmOutput(logTag, vm.getLogOutput(), "Log");
             mExecutorService.awaitTermination(300, TimeUnit.SECONDS);
         }
 
@@ -232,7 +232,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 +330,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/java/com/android/microdroid/test/MicrodroidHostTests.java b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
index f9de77e..a836559 100644
--- a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
+++ b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
@@ -465,7 +465,9 @@
 
         // check until microdroid is shut down
         CommandRunner android = new CommandRunner(getDevice());
-        android.runWithTimeout(15000, "logcat", "-m", "1", "-e", "'crosvm has exited normally'");
+        // TODO: improve crosvm exit check. b/258848245
+        android.runWithTimeout(15000, "logcat", "-m", "1", "-e",
+                              "'virtualizationservice::crosvm.*exited with status exit status: 0'");
         // Check that tombstone is received (from host logcat)
         String result =
                 runOnHost(
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..492eb33 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;
@@ -600,7 +600,7 @@
     }
 
     @Test
-    public void vmConvertsToValidParcelVm() throws Exception {
+    public void vmConvertsToValidDescriptor() throws Exception {
         // Arrange
         VirtualMachineConfig config =
                 mInner.newVmConfigBuilder()
@@ -611,11 +611,11 @@
         VirtualMachine vm = mInner.forceCreateNewVirtualMachine(vmName, config);
 
         // Action
-        ParcelVirtualMachine parcelVm = vm.toParcelVirtualMachine();
+        VirtualMachineDescriptor descriptor = vm.toDescriptor();
 
         // Asserts
-        assertFileContentsAreEqual(parcelVm.getConfigFd(), vmName, "config.xml");
-        assertFileContentsAreEqual(parcelVm.getInstanceImgFd(), vmName, "instance.img");
+        assertFileContentsAreEqual(descriptor.getConfigFd(), vmName, "config.xml");
+        assertFileContentsAreEqual(descriptor.getInstanceImgFd(), vmName, "instance.img");
     }
 
     private void assertFileContentsAreEqual(
@@ -671,8 +671,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 +696,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/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/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 bc697e3..340fc68 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)
@@ -854,11 +821,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 +860,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 +1048,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_start_timestamp.lock().unwrap();
             write_vm_booted_stats(vm.requester_uid as i32, &vm.name, *vm_start_timestamp);
@@ -1140,6 +1115,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/crosvm.rs b/virtualizationservice/src/crosvm.rs
index 1b8061e..29040b7 100644
--- a/virtualizationservice/src/crosvm.rs
+++ b/virtualizationservice/src/crosvm.rs
@@ -35,7 +35,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;
@@ -190,8 +189,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 timestamp when the VM is started.
@@ -223,7 +220,6 @@
             requester_uid,
             requester_debug_pid,
             callbacks: Default::default(),
-            stream: Mutex::new(None),
             vm_service: Mutex::new(None),
             vm_start_timestamp: Mutex::new(None),
             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(())
     }