Merge changes from topic "microdroid_log"

* changes:
  Add TestApis for using console input
  Support console input to VM
diff --git a/compos/common/compos_client.rs b/compos/common/compos_client.rs
index 77a1204..a8a176a 100644
--- a/compos/common/compos_client.rs
+++ b/compos/common/compos_client.rs
@@ -140,8 +140,15 @@
         // Let logs go to logcat.
         let (console_fd, log_fd) = (None, None);
         let callback = Box::new(Callback {});
-        let instance = VmInstance::create(service, &config, console_fd, log_fd, Some(callback))
-            .context("Failed to create VM")?;
+        let instance = VmInstance::create(
+            service,
+            &config,
+            console_fd,
+            /*console_in_fd */ None,
+            log_fd,
+            Some(callback),
+        )
+        .context("Failed to create VM")?;
 
         instance.start()?;
 
diff --git a/demo_native/main.cpp b/demo_native/main.cpp
index fa87549..bc42036 100644
--- a/demo_native/main.cpp
+++ b/demo_native/main.cpp
@@ -223,10 +223,11 @@
     std::shared_ptr<IVirtualMachine> vm;
 
     VirtualMachineConfig config = std::move(app_config);
-    ScopedFileDescriptor console_fd(fcntl(fileno(stdout), F_DUPFD_CLOEXEC));
+    ScopedFileDescriptor console_out_fd(fcntl(fileno(stdout), F_DUPFD_CLOEXEC));
+    ScopedFileDescriptor console_in_fd(fcntl(fileno(stdin), F_DUPFD_CLOEXEC));
     ScopedFileDescriptor log_fd(fcntl(fileno(stdout), F_DUPFD_CLOEXEC));
 
-    ScopedAStatus ret = service.createVm(config, console_fd, log_fd, &vm);
+    ScopedAStatus ret = service.createVm(config, console_out_fd, console_in_fd, log_fd, &vm);
     if (!ret.isOk()) {
         return Error() << "Failed to create VM";
     }
diff --git a/javalib/api/test-current.txt b/javalib/api/test-current.txt
index 8b7ec11..1298000 100644
--- a/javalib/api/test-current.txt
+++ b/javalib/api/test-current.txt
@@ -2,15 +2,18 @@
 package android.system.virtualmachine {
 
   public class VirtualMachine implements java.lang.AutoCloseable {
+    method @NonNull @WorkerThread public java.io.OutputStream getConsoleInput() throws android.system.virtualmachine.VirtualMachineException;
     method @NonNull public java.io.File getRootDir();
   }
 
   public final class VirtualMachineConfig {
     method @Nullable public String getPayloadConfigPath();
+    method public boolean isVmConsoleInputSupported();
   }
 
   public static final class VirtualMachineConfig.Builder {
     method @NonNull @RequiresPermission(android.system.virtualmachine.VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION) public android.system.virtualmachine.VirtualMachineConfig.Builder setPayloadConfigPath(@NonNull String);
+    method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setVmConsoleInputSupported(boolean);
   }
 
 }
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachine.java b/javalib/src/android/system/virtualmachine/VirtualMachine.java
index f96effa..675a046 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachine.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachine.java
@@ -76,11 +76,13 @@
 
 import java.io.File;
 import java.io.FileInputStream;
+import java.io.FileOutputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.io.OutputStream;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.nio.channels.FileChannel;
@@ -294,6 +296,8 @@
 
     private final boolean mVmOutputCaptured;
 
+    private final boolean mVmConsoleInputSupported;
+
     /** The configuration that is currently associated with this VM. */
     @GuardedBy("mLock")
     @NonNull
@@ -306,11 +310,19 @@
 
     @GuardedBy("mLock")
     @Nullable
-    private ParcelFileDescriptor mConsoleReader;
+    private ParcelFileDescriptor mConsoleOutReader;
 
     @GuardedBy("mLock")
     @Nullable
-    private ParcelFileDescriptor mConsoleWriter;
+    private ParcelFileDescriptor mConsoleOutWriter;
+
+    @GuardedBy("mLock")
+    @Nullable
+    private ParcelFileDescriptor mConsoleInReader;
+
+    @GuardedBy("mLock")
+    @Nullable
+    private ParcelFileDescriptor mConsoleInWriter;
 
     @GuardedBy("mLock")
     @Nullable
@@ -372,6 +384,7 @@
                         : null;
 
         mVmOutputCaptured = config.isVmOutputCaptured();
+        mVmConsoleInputSupported = config.isVmConsoleInputSupported();
     }
 
     /**
@@ -787,7 +800,11 @@
 
             try {
                 if (mVmOutputCaptured) {
-                    createVmPipes();
+                    createVmOutputPipes();
+                }
+
+                if (mVmConsoleInputSupported) {
+                    createVmInputPipes();
                 }
 
                 VirtualMachineAppConfig appConfig =
@@ -804,7 +821,9 @@
                         android.system.virtualizationservice.VirtualMachineConfig.appConfig(
                                 appConfig);
 
-                mVirtualMachine = service.createVm(vmConfigParcel, mConsoleWriter, mLogWriter);
+                mVirtualMachine =
+                        service.createVm(
+                                vmConfigParcel, mConsoleOutWriter, mConsoleInReader, mLogWriter);
                 mVirtualMachine.registerCallback(new CallbackTranslator(service));
                 mContext.registerComponentCallbacks(mMemoryManagementCallbacks);
                 mVirtualMachine.start();
@@ -843,12 +862,12 @@
     }
 
     @GuardedBy("mLock")
-    private void createVmPipes() throws VirtualMachineException {
+    private void createVmOutputPipes() throws VirtualMachineException {
         try {
-            if (mConsoleReader == null || mConsoleWriter == null) {
+            if (mConsoleOutReader == null || mConsoleOutWriter == null) {
                 ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
-                mConsoleReader = pipe[0];
-                mConsoleWriter = pipe[1];
+                mConsoleOutReader = pipe[0];
+                mConsoleOutWriter = pipe[1];
             }
 
             if (mLogReader == null || mLogWriter == null) {
@@ -857,7 +876,20 @@
                 mLogWriter = pipe[1];
             }
         } catch (IOException e) {
-            throw new VirtualMachineException("Failed to create stream for VM", e);
+            throw new VirtualMachineException("Failed to create output stream for VM", e);
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void createVmInputPipes() throws VirtualMachineException {
+        try {
+            if (mConsoleInReader == null || mConsoleInWriter == null) {
+                ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
+                mConsoleInReader = pipe[0];
+                mConsoleInWriter = pipe[1];
+            }
+        } catch (IOException e) {
+            throw new VirtualMachineException("Failed to create input stream for VM", e);
         }
     }
 
@@ -883,12 +915,37 @@
             throw new VirtualMachineException("Capturing vm outputs is turned off");
         }
         synchronized (mLock) {
-            createVmPipes();
-            return new FileInputStream(mConsoleReader.getFileDescriptor());
+            createVmOutputPipes();
+            return new FileInputStream(mConsoleOutReader.getFileDescriptor());
         }
     }
 
     /**
+     * Returns the stream object representing the console input to the virtual machine. The console
+     * input is only available if the {@link VirtualMachineConfig} specifies that it should be
+     * {@linkplain VirtualMachineConfig#isVmConsoleInputSupported supported}.
+     *
+     * <p>NOTE: This method may block and should not be called on the main thread.
+     *
+     * @throws VirtualMachineException if the stream could not be created, or console input is not
+     *     supported.
+     * @hide
+     */
+    @TestApi
+    @WorkerThread
+    @NonNull
+    public OutputStream getConsoleInput() throws VirtualMachineException {
+        if (!mVmConsoleInputSupported) {
+            throw new VirtualMachineException("VM console input is not supported");
+        }
+        synchronized (mLock) {
+            createVmInputPipes();
+            return new FileOutputStream(mConsoleInWriter.getFileDescriptor());
+        }
+    }
+
+
+    /**
      * Returns the stream object representing the log output from the virtual machine. The log
      * output is only available if the VirtualMachineConfig specifies that it should be {@linkplain
      * VirtualMachineConfig#isVmOutputCaptured captured}.
@@ -910,7 +967,7 @@
             throw new VirtualMachineException("Capturing vm outputs is turned off");
         }
         synchronized (mLock) {
-            createVmPipes();
+            createVmOutputPipes();
             return new FileInputStream(mLogReader.getFileDescriptor());
         }
     }
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
index 5f24f5b..b400eeb 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -75,6 +75,7 @@
     private static final String KEY_CPU_TOPOLOGY = "cpuTopology";
     private static final String KEY_ENCRYPTED_STORAGE_BYTES = "encryptedStorageBytes";
     private static final String KEY_VM_OUTPUT_CAPTURED = "vmOutputCaptured";
+    private static final String KEY_VM_CONSOLE_INPUT_SUPPORTED = "vmConsoleInputSupported";
 
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
@@ -163,6 +164,9 @@
     /** Whether the app can read console and log output. */
     private final boolean mVmOutputCaptured;
 
+    /** Whether the app can write console input to the VM */
+    private final boolean mVmConsoleInputSupported;
+
     private VirtualMachineConfig(
             @Nullable String packageName,
             @Nullable String apkPath,
@@ -173,7 +177,8 @@
             long memoryBytes,
             @CpuTopology int cpuTopology,
             long encryptedStorageBytes,
-            boolean vmOutputCaptured) {
+            boolean vmOutputCaptured,
+            boolean vmConsoleInputSupported) {
         // This is only called from Builder.build(); the builder handles parameter validation.
         mPackageName = packageName;
         mApkPath = apkPath;
@@ -185,6 +190,7 @@
         mCpuTopology = cpuTopology;
         mEncryptedStorageBytes = encryptedStorageBytes;
         mVmOutputCaptured = vmOutputCaptured;
+        mVmConsoleInputSupported = vmConsoleInputSupported;
     }
 
     /** Loads a config from a file. */
@@ -259,6 +265,7 @@
             builder.setEncryptedStorageBytes(encryptedStorageBytes);
         }
         builder.setVmOutputCaptured(b.getBoolean(KEY_VM_OUTPUT_CAPTURED));
+        builder.setVmConsoleInputSupported(b.getBoolean(KEY_VM_CONSOLE_INPUT_SUPPORTED));
 
         return builder.build();
     }
@@ -294,6 +301,7 @@
             b.putLong(KEY_ENCRYPTED_STORAGE_BYTES, mEncryptedStorageBytes);
         }
         b.putBoolean(KEY_VM_OUTPUT_CAPTURED, mVmOutputCaptured);
+        b.putBoolean(KEY_VM_CONSOLE_INPUT_SUPPORTED, mVmConsoleInputSupported);
         b.writeToStream(output);
     }
 
@@ -412,6 +420,17 @@
     }
 
     /**
+     * Returns whether the app can write to the VM console.
+     *
+     * @see Builder#setVmConsoleInputSupported
+     * @hide
+     */
+    @TestApi
+    public boolean isVmConsoleInputSupported() {
+        return mVmConsoleInputSupported;
+    }
+
+    /**
      * Tests if this config is compatible with other config. Being compatible means that the configs
      * can be interchangeably used for the same virtual machine; they do not change the VM identity
      * or secrets. Such changes include varying the number of CPUs or the size of the RAM. Changes
@@ -430,6 +449,7 @@
                 && this.mProtectedVm == other.mProtectedVm
                 && this.mEncryptedStorageBytes == other.mEncryptedStorageBytes
                 && this.mVmOutputCaptured == other.mVmOutputCaptured
+                && this.mVmConsoleInputSupported == other.mVmConsoleInputSupported
                 && Objects.equals(this.mPayloadConfigPath, other.mPayloadConfigPath)
                 && Objects.equals(this.mPayloadBinaryName, other.mPayloadBinaryName)
                 && Objects.equals(this.mPackageName, other.mPackageName)
@@ -551,6 +571,7 @@
         @CpuTopology private int mCpuTopology = CPU_TOPOLOGY_ONE_CPU;
         private long mEncryptedStorageBytes;
         private boolean mVmOutputCaptured = false;
+        private boolean mVmConsoleInputSupported = false;
 
         /**
          * Creates a builder for the given context.
@@ -609,6 +630,10 @@
                 throw new IllegalStateException("debug level must be FULL to capture output");
             }
 
+            if (mVmConsoleInputSupported && mDebugLevel != DEBUG_LEVEL_FULL) {
+                throw new IllegalStateException("debug level must be FULL to use console input");
+            }
+
             return new VirtualMachineConfig(
                     packageName,
                     apkPath,
@@ -619,7 +644,8 @@
                     mMemoryBytes,
                     mCpuTopology,
                     mEncryptedStorageBytes,
-                    mVmOutputCaptured);
+                    mVmOutputCaptured,
+                    mVmConsoleInputSupported);
         }
 
         /**
@@ -819,5 +845,23 @@
             mVmOutputCaptured = captured;
             return this;
         }
+
+        /**
+         * Sets whether to allow the app to write to the VM console. Default is {@code false}.
+         *
+         * <p>Setting this as {@code true} will allow the app to directly write into {@linkplain
+         * VirtualMachine#getConsoleInput console input}.
+         *
+         * <p>The {@linkplain #setDebugLevel debug level} must be {@link #DEBUG_LEVEL_FULL} to be
+         * set as true.
+         *
+         * @hide
+         */
+        @TestApi
+        @NonNull
+        public Builder setVmConsoleInputSupported(boolean supported) {
+            mVmConsoleInputSupported = supported;
+            return this;
+        }
     }
 }
diff --git a/rialto/tests/test.rs b/rialto/tests/test.rs
index 7048b44..98c935d 100644
--- a/rialto/tests/test.rs
+++ b/rialto/tests/test.rs
@@ -114,8 +114,15 @@
         taskProfiles: vec![],
         gdbPort: 0, // No gdb
     });
-    let vm = VmInstance::create(service.as_ref(), &config, Some(console), Some(log), None)
-        .context("Failed to create VM")?;
+    let vm = VmInstance::create(
+        service.as_ref(),
+        &config,
+        Some(console),
+        /* consoleIn */ None,
+        Some(log),
+        None,
+    )
+    .context("Failed to create VM")?;
 
     vm.start().context("Failed to start VM")?;
 
diff --git a/tests/aidl/com/android/microdroid/testservice/ITestService.aidl b/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
index a6f1c80..8d467cd 100644
--- a/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
+++ b/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
@@ -70,6 +70,9 @@
     /** Requests the VM to asynchronously call appCallback.setVmCallback() */
     void requestCallback(IAppCallback appCallback);
 
+    /** Read a line from /dev/console */
+    String readLineFromConsole();
+
     /**
      * Request the service to exit, triggering the termination of the VM. This may cause any
      * requests in flight to fail.
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 7e6080f..e6d90ea 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
@@ -456,6 +456,7 @@
         public long[] mTimings;
         public int mFileMode;
         public int mMountFlags;
+        public String mConsoleInput;
 
         public void assertNoException() {
             if (mException != null) {
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 a64b62a..ffb2c11 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -1652,6 +1652,35 @@
     }
 
     @Test
+    public void testConsoleInputSupported() throws Exception {
+        assumeSupportedDevice();
+
+        VirtualMachineConfig config =
+                newVmConfigBuilder()
+                        .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .setVmConsoleInputSupported(true)
+                        .setVmOutputCaptured(true)
+                        .build();
+        VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_console_in", config);
+
+        final String TYPED = "this is a console input\n";
+        TestResults testResults =
+                runVmTestService(
+                        TAG,
+                        vm,
+                        (ts, tr) -> {
+                            OutputStreamWriter consoleIn =
+                                    new OutputStreamWriter(vm.getConsoleInput());
+                            consoleIn.write(TYPED);
+                            consoleIn.close();
+                            tr.mConsoleInput = ts.readLineFromConsole();
+                        });
+        testResults.assertNoException();
+        assertThat(testResults.mConsoleInput).isEqualTo(TYPED);
+    }
+
+    @Test
     public void testStartVmWithPayloadOfAnotherApp() throws Exception {
         assumeSupportedDevice();
 
diff --git a/tests/testapk/src/native/testbinary.cpp b/tests/testapk/src/native/testbinary.cpp
index 7e0fc5b..297b505 100644
--- a/tests/testapk/src/native/testbinary.cpp
+++ b/tests/testapk/src/native/testbinary.cpp
@@ -313,6 +313,26 @@
             return ScopedAStatus::ok();
         }
 
+        ScopedAStatus readLineFromConsole(std::string* out) {
+            FILE* f = fopen("/dev/console", "r");
+            if (f == nullptr) {
+                return ScopedAStatus::fromExceptionCodeWithMessage(EX_SERVICE_SPECIFIC,
+                                                                   "failed to open /dev/console");
+            }
+            char* line = nullptr;
+            size_t len = 0;
+            ssize_t nread = getline(&line, &len, f);
+
+            if (nread == -1) {
+                free(line);
+                return ScopedAStatus::fromExceptionCodeWithMessage(EX_SERVICE_SPECIFIC,
+                                                                   "failed to read /dev/console");
+            }
+            out->append(line, nread);
+            free(line);
+            return ScopedAStatus::ok();
+        }
+
         ScopedAStatus quit() override { exit(0); }
     };
     auto testService = ndk::SharedRefBase::make<TestService>();
diff --git a/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java b/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java
index 1f7ffb7..0ddf70b 100644
--- a/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java
+++ b/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java
@@ -245,6 +245,11 @@
         }
 
         @Override
+        public String readLineFromConsole() {
+            throw new UnsupportedOperationException("Not supported");
+        }
+
+        @Override
         public void quit() throws RemoteException {
             throw new UnsupportedOperationException("Not supported");
         }
diff --git a/virtualizationmanager/src/aidl.rs b/virtualizationmanager/src/aidl.rs
index 968d2d2..dd74d55 100644
--- a/virtualizationmanager/src/aidl.rs
+++ b/virtualizationmanager/src/aidl.rs
@@ -186,11 +186,18 @@
     fn createVm(
         &self,
         config: &VirtualMachineConfig,
-        console_fd: Option<&ParcelFileDescriptor>,
+        console_out_fd: Option<&ParcelFileDescriptor>,
+        console_in_fd: Option<&ParcelFileDescriptor>,
         log_fd: Option<&ParcelFileDescriptor>,
     ) -> binder::Result<Strong<dyn IVirtualMachine>> {
         let mut is_protected = false;
-        let ret = self.create_vm_internal(config, console_fd, log_fd, &mut is_protected);
+        let ret = self.create_vm_internal(
+            config,
+            console_out_fd,
+            console_in_fd,
+            log_fd,
+            &mut is_protected,
+        );
         write_vm_creation_stats(config, is_protected, &ret);
         ret
     }
@@ -303,7 +310,8 @@
     fn create_vm_internal(
         &self,
         config: &VirtualMachineConfig,
-        console_fd: Option<&ParcelFileDescriptor>,
+        console_out_fd: Option<&ParcelFileDescriptor>,
+        console_in_fd: Option<&ParcelFileDescriptor>,
         log_fd: Option<&ParcelFileDescriptor>,
         is_protected: &mut bool,
     ) -> binder::Result<Strong<dyn IVirtualMachine>> {
@@ -350,8 +358,9 @@
         };
 
         let state = &mut *self.state.lock().unwrap();
-        let console_fd =
-            clone_or_prepare_logger_fd(&debug_config, console_fd, format!("Console({})", cid))?;
+        let console_out_fd =
+            clone_or_prepare_logger_fd(&debug_config, console_out_fd, format!("Console({})", cid))?;
+        let console_in_fd = console_in_fd.map(clone_file).transpose()?;
         let log_fd = clone_or_prepare_logger_fd(&debug_config, log_fd, format!("Log({})", cid))?;
 
         // Counter to generate unique IDs for temporary image files.
@@ -455,7 +464,8 @@
             cpus,
             host_cpu_topology,
             task_profiles: config.taskProfiles.clone(),
-            console_fd,
+            console_out_fd,
+            console_in_fd,
             log_fd,
             ramdump,
             indirect_files,
diff --git a/virtualizationmanager/src/crosvm.rs b/virtualizationmanager/src/crosvm.rs
index 856ff1e..13367c3 100644
--- a/virtualizationmanager/src/crosvm.rs
+++ b/virtualizationmanager/src/crosvm.rs
@@ -107,7 +107,8 @@
     pub cpus: Option<NonZeroU32>,
     pub host_cpu_topology: bool,
     pub task_profiles: Vec<String>,
-    pub console_fd: Option<File>,
+    pub console_out_fd: Option<File>,
+    pub console_in_fd: Option<File>,
     pub log_fd: Option<File>,
     pub ramdump: Option<File>,
     pub indirect_files: Vec<File>,
@@ -776,21 +777,29 @@
     //
     // When [console|log]_fd is not specified, the devices are attached to sink, which means what's
     // written there is discarded.
-    let console_arg = format_serial_arg(&mut preserved_fds, &config.console_fd);
-    let log_arg = format_serial_arg(&mut preserved_fds, &config.log_fd);
+    let console_out_arg = format_serial_out_arg(&mut preserved_fds, &config.console_out_fd);
+    let console_in_arg = config
+        .console_in_fd
+        .as_ref()
+        .map(|fd| format!(",input={}", add_preserved_fd(&mut preserved_fds, fd)))
+        .unwrap_or_default();
+    let log_arg = format_serial_out_arg(&mut preserved_fds, &config.log_fd);
     let failure_serial_path = add_preserved_fd(&mut preserved_fds, &failure_pipe_write);
-    let ramdump_arg = format_serial_arg(&mut preserved_fds, &config.ramdump);
+    let ramdump_arg = format_serial_out_arg(&mut preserved_fds, &config.ramdump);
 
     // Warning: Adding more serial devices requires you to shift the PCI device ID of the boot
     // disks in bootconfig.x86_64. This is because x86 crosvm puts serial devices and the block
     // devices in the same PCI bus and serial devices comes before the block devices. Arm crosvm
     // doesn't have the issue.
     // /dev/ttyS0
-    command.arg(format!("--serial={},hardware=serial,num=1", &console_arg));
+    command.arg(format!("--serial={}{},hardware=serial,num=1", &console_out_arg, &console_in_arg));
     // /dev/ttyS1
     command.arg(format!("--serial=type=file,path={},hardware=serial,num=2", &failure_serial_path));
     // /dev/hvc0
-    command.arg(format!("--serial={},hardware=virtio-console,num=1", &console_arg));
+    command.arg(format!(
+        "--serial={}{},hardware=virtio-console,num=1",
+        &console_out_arg, &console_in_arg
+    ));
     // /dev/hvc1
     command.arg(format!("--serial={},hardware=virtio-console,num=2", &ramdump_arg));
     // /dev/hvc2
@@ -890,7 +899,7 @@
 
 /// Adds the file descriptor for `file` (if any) to `preserved_fds`, and returns the appropriate
 /// string for a crosvm `--serial` flag. If `file` is none, creates a dummy sink device.
-fn format_serial_arg(preserved_fds: &mut Vec<RawFd>, file: &Option<File>) -> String {
+fn format_serial_out_arg(preserved_fds: &mut Vec<RawFd>, file: &Option<File>) -> String {
     if let Some(file) = file {
         format!("type=file,path={}", add_preserved_fd(preserved_fds, file))
     } else {
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
index d72d5ac..99bc076 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
@@ -23,12 +23,14 @@
 interface IVirtualizationService {
     /**
      * Create the VM with the given config file, and return a handle to it ready to start it. If
-     * `consoleFd` is provided then console output from the VM will be sent to it. If `osLogFd` is
+     * `consoleOutFd` is provided then console output from the VM will be sent to it. If
+     * `consoleInFd` is provided then console input to the VM will be read from it. If `osLogFd` is
      * provided then the OS-level logs will be sent to it. `osLogFd` is supported only when the OS
      * running in the VM has the logging system. In case of Microdroid, the logging system is logd.
      */
     IVirtualMachine createVm(in VirtualMachineConfig config,
-            in @nullable ParcelFileDescriptor consoleFd,
+            in @nullable ParcelFileDescriptor consoleOutFd,
+            in @nullable ParcelFileDescriptor consoleInFd,
             in @nullable ParcelFileDescriptor osLogFd);
 
     /**
diff --git a/virtualizationservice/src/rkpvm.rs b/virtualizationservice/src/rkpvm.rs
index a4649f6..ebfb667 100644
--- a/virtualizationservice/src/rkpvm.rs
+++ b/virtualizationservice/src/rkpvm.rs
@@ -79,7 +79,7 @@
         taskProfiles: vec![],
         gdbPort: 0, // No gdb
     });
-    let vm = VmInstance::create(service.as_ref(), &config, None, None, None)
+    let vm = VmInstance::create(service.as_ref(), &config, None, None, None, None)
         .context("Failed to create service VM")?;
 
     info!("service_vm: Starting Service VM...");
diff --git a/vm/src/main.rs b/vm/src/main.rs
index bc3f4da..0800f57 100644
--- a/vm/src/main.rs
+++ b/vm/src/main.rs
@@ -74,6 +74,10 @@
         #[clap(long)]
         console: Option<PathBuf>,
 
+        /// Path to file for VM console input.
+        #[clap(long)]
+        console_in: Option<PathBuf>,
+
         /// Path to file for VM log output.
         #[clap(long)]
         log: Option<PathBuf>,
@@ -138,6 +142,10 @@
         #[clap(long)]
         console: Option<PathBuf>,
 
+        /// Path to file for VM console input.
+        #[clap(long)]
+        console_in: Option<PathBuf>,
+
         /// Path to file for VM log output.
         #[clap(long)]
         log: Option<PathBuf>,
@@ -193,6 +201,10 @@
         #[clap(long)]
         console: Option<PathBuf>,
 
+        /// Path to file for VM console input.
+        #[clap(long)]
+        console_in: Option<PathBuf>,
+
         /// Path to file for VM log output.
         #[clap(long)]
         log: Option<PathBuf>,
@@ -277,6 +289,7 @@
             config_path,
             payload_binary_name,
             console,
+            console_in,
             log,
             debug,
             protected,
@@ -297,6 +310,7 @@
             config_path,
             payload_binary_name,
             console.as_deref(),
+            console_in.as_deref(),
             log.as_deref(),
             debug,
             protected,
@@ -313,6 +327,7 @@
             storage,
             storage_size,
             console,
+            console_in,
             log,
             debug,
             protected,
@@ -328,6 +343,7 @@
             storage.as_deref(),
             storage_size,
             console.as_deref(),
+            console_in.as_deref(),
             log.as_deref(),
             debug,
             protected,
@@ -337,12 +353,13 @@
             gdb,
             kernel.as_deref(),
         ),
-        Opt::Run { name, config, cpu_topology, task_profiles, console, log, gdb } => {
+        Opt::Run { name, config, cpu_topology, task_profiles, console, console_in, log, gdb } => {
             command_run(
                 name,
                 get_service()?.as_ref(),
                 &config,
                 console.as_deref(),
+                console_in.as_deref(),
                 log.as_deref(),
                 /* mem */ None,
                 cpu_topology,
diff --git a/vm/src/run.rs b/vm/src/run.rs
index 392fa1c..84072ca 100644
--- a/vm/src/run.rs
+++ b/vm/src/run.rs
@@ -54,7 +54,8 @@
     storage_size: Option<u64>,
     config_path: Option<String>,
     payload_binary_name: Option<String>,
-    console_path: Option<&Path>,
+    console_out_path: Option<&Path>,
+    console_in_path: Option<&Path>,
     log_path: Option<&Path>,
     debug_level: DebugLevel,
     protected: bool,
@@ -159,7 +160,7 @@
         cpuTopology: cpu_topology,
         customConfig: Some(custom_config),
     });
-    run(service, &config, &payload_config_str, console_path, log_path)
+    run(service, &config, &payload_config_str, console_out_path, console_in_path, log_path)
 }
 
 fn find_empty_payload_apk_path() -> Result<PathBuf, Error> {
@@ -192,7 +193,8 @@
     work_dir: Option<PathBuf>,
     storage: Option<&Path>,
     storage_size: Option<u64>,
-    console_path: Option<&Path>,
+    console_out_path: Option<&Path>,
+    console_in_path: Option<&Path>,
     log_path: Option<&Path>,
     debug_level: DebugLevel,
     protected: bool,
@@ -223,7 +225,8 @@
         storage_size,
         /* config_path= */ None,
         Some(payload_binary_name.to_owned()),
-        console_path,
+        console_out_path,
+        console_in_path,
         log_path,
         debug_level,
         protected,
@@ -242,7 +245,8 @@
     name: Option<String>,
     service: &dyn IVirtualizationService,
     config_path: &Path,
-    console_path: Option<&Path>,
+    console_out_path: Option<&Path>,
+    console_in_path: Option<&Path>,
     log_path: Option<&Path>,
     mem: Option<u32>,
     cpu_topology: CpuTopology,
@@ -269,7 +273,8 @@
         service,
         &VirtualMachineConfig::RawConfig(config),
         &format!("{:?}", config_path),
-        console_path,
+        console_out_path,
+        console_in_path,
         log_path,
     )
 }
@@ -290,28 +295,35 @@
     service: &dyn IVirtualizationService,
     config: &VirtualMachineConfig,
     payload_config: &str,
-    console_path: Option<&Path>,
+    console_out_path: Option<&Path>,
+    console_in_path: Option<&Path>,
     log_path: Option<&Path>,
 ) -> Result<(), Error> {
-    let console = if let Some(console_path) = console_path {
-        Some(
-            File::create(console_path)
-                .with_context(|| format!("Failed to open console file {:?}", console_path))?,
-        )
+    let console_out = if let Some(console_out_path) = console_out_path {
+        Some(File::create(console_out_path).with_context(|| {
+            format!("Failed to open console output file {:?}", console_out_path)
+        })?)
     } else {
-        Some(duplicate_stdout()?)
+        Some(duplicate_fd(io::stdout())?)
     };
+    let console_in =
+        if let Some(console_in_path) = console_in_path {
+            Some(File::create(console_in_path).with_context(|| {
+                format!("Failed to open console input file {:?}", console_in_path)
+            })?)
+        } else {
+            Some(duplicate_fd(io::stdin())?)
+        };
     let log = if let Some(log_path) = log_path {
         Some(
             File::create(log_path)
                 .with_context(|| format!("Failed to open log file {:?}", log_path))?,
         )
     } else {
-        Some(duplicate_stdout()?)
+        Some(duplicate_fd(io::stdout())?)
     };
-
     let callback = Box::new(Callback {});
-    let vm = VmInstance::create(service, config, console, log, Some(callback))
+    let vm = VmInstance::create(service, config, console_out, console_in, log, Some(callback))
         .context("Failed to create VM")?;
     vm.start().context("Failed to start VM")?;
 
@@ -361,12 +373,12 @@
     }
 }
 
-/// Safely duplicate the standard output file descriptor.
-fn duplicate_stdout() -> io::Result<File> {
-    let stdout_fd = io::stdout().as_raw_fd();
+/// Safely duplicate the file descriptor.
+fn duplicate_fd<T: AsRawFd>(file: T) -> io::Result<File> {
+    let fd = file.as_raw_fd();
     // Safe because this just duplicates a file descriptor which we know to be valid, and we check
     // for an error.
-    let dup_fd = unsafe { libc::dup(stdout_fd) };
+    let dup_fd = unsafe { libc::dup(fd) };
     if dup_fd < 0 {
         Err(io::Error::last_os_error())
     } else {
diff --git a/vmbase/example/tests/test.rs b/vmbase/example/tests/test.rs
index 085a620..3594523 100644
--- a/vmbase/example/tests/test.rs
+++ b/vmbase/example/tests/test.rs
@@ -106,8 +106,15 @@
     });
     let (handle, console) = android_log_fd()?;
     let (mut log_reader, log_writer) = pipe()?;
-    let vm = VmInstance::create(service.as_ref(), &config, Some(console), Some(log_writer), None)
-        .context("Failed to create VM")?;
+    let vm = VmInstance::create(
+        service.as_ref(),
+        &config,
+        Some(console),
+        /* consoleIn */ None,
+        Some(log_writer),
+        None,
+    )
+    .context("Failed to create VM")?;
     vm.start().context("Failed to start VM")?;
     info!("Started example VM.");
 
diff --git a/vmclient/src/lib.rs b/vmclient/src/lib.rs
index 8f25b99..cfd015a 100644
--- a/vmclient/src/lib.rs
+++ b/vmclient/src/lib.rs
@@ -175,14 +175,17 @@
     pub fn create(
         service: &dyn IVirtualizationService,
         config: &VirtualMachineConfig,
-        console: Option<File>,
+        console_out: Option<File>,
+        console_in: Option<File>,
         log: Option<File>,
         callback: Option<Box<dyn VmCallback + Send + Sync>>,
     ) -> BinderResult<Self> {
-        let console = console.map(ParcelFileDescriptor::new);
+        let console_out = console_out.map(ParcelFileDescriptor::new);
+        let console_in = console_in.map(ParcelFileDescriptor::new);
         let log = log.map(ParcelFileDescriptor::new);
 
-        let vm = service.createVm(config, console.as_ref(), log.as_ref())?;
+        let vm =
+            service.createVm(config, console_out.as_ref(), console_in.as_ref(), log.as_ref())?;
 
         let cid = vm.getCid()?;