Merge "Ensure vbmeta_digest is not empty" into main
diff --git a/apex/vmnic.rc b/apex/vmnic.rc
index 486f387..f5dfd99 100644
--- a/apex/vmnic.rc
+++ b/apex/vmnic.rc
@@ -13,8 +13,8 @@
 # limitations under the License.
 
 service vmnic /apex/com.android.virt/bin/vmnic
-    user system
-    group system
+    user root
+    group vpn
     interface aidl android.system.virtualizationservice_internal.IVmnic
     disabled
     oneshot
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachine.java b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
index 2f6e306..43f3db0 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachine.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
@@ -42,8 +42,8 @@
 
 import static java.util.Objects.requireNonNull;
 
-import android.annotation.FlaggedApi;
 import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
@@ -63,8 +63,8 @@
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
+import android.system.ErrnoException;
+import android.system.OsConstants;
 import android.system.virtualizationcommon.DeathReason;
 import android.system.virtualizationcommon.ErrorCode;
 import android.system.virtualizationservice.IVirtualMachine;
@@ -78,13 +78,17 @@
 import android.system.virtualizationservice.VirtualMachineState;
 import android.util.JsonReader;
 import android.util.Log;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.system.virtualmachine.flags.Flags;
 
 import libcore.io.IoBridge;
+import libcore.io.IoUtils;
 
 import java.io.File;
+import java.io.FileDescriptor;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
@@ -97,17 +101,20 @@
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.nio.channels.FileChannel;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.FileAlreadyExistsException;
 import java.nio.file.FileVisitResult;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.SimpleFileVisitor;
 import java.nio.file.attribute.BasicFileAttributes;
-import java.util.Arrays;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Consumer;
 import java.util.zip.ZipFile;
@@ -320,6 +327,10 @@
 
     private final boolean mVmConsoleInputSupported;
 
+    private final boolean mConnectVmConsole;
+
+    private final Executor mConsoleExecutor = Executors.newSingleThreadExecutor();
+
     /** The configuration that is currently associated with this VM. */
     @GuardedBy("mLock")
     @NonNull
@@ -348,6 +359,26 @@
 
     @GuardedBy("mLock")
     @Nullable
+    private ParcelFileDescriptor mTeeConsoleOutReader;
+
+    @GuardedBy("mLock")
+    @Nullable
+    private ParcelFileDescriptor mTeeConsoleOutWriter;
+
+    @GuardedBy("mLock")
+    @Nullable
+    private ParcelFileDescriptor mPtyFd;
+
+    @GuardedBy("mLock")
+    @Nullable
+    private ParcelFileDescriptor mPtsFd;
+
+    @GuardedBy("mLock")
+    @Nullable
+    private String mPtsName;
+
+    @GuardedBy("mLock")
+    @Nullable
     private ParcelFileDescriptor mLogReader;
 
     @GuardedBy("mLock")
@@ -417,6 +448,7 @@
 
         mVmOutputCaptured = config.isVmOutputCaptured();
         mVmConsoleInputSupported = config.isVmConsoleInputSupported();
+        mConnectVmConsole = config.isConnectVmConsole();
     }
 
     /**
@@ -1128,6 +1160,10 @@
             IVirtualizationService service = mVirtualizationService.getBinder();
 
             try {
+                if (mConnectVmConsole) {
+                    createPtyConsole();
+                }
+
                 if (mVmOutputCaptured) {
                     createVmOutputPipes();
                 }
@@ -1136,6 +1172,38 @@
                     createVmInputPipes();
                 }
 
+                ParcelFileDescriptor consoleOutFd = null;
+                if (mConnectVmConsole && mVmOutputCaptured) {
+                    // If we are enabling output pipes AND the host console, then we tee the console
+                    // output to both.
+                    ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
+                    mTeeConsoleOutReader = pipe[0];
+                    mTeeConsoleOutWriter = pipe[1];
+                    consoleOutFd = mTeeConsoleOutWriter;
+                    TeeWorker tee =
+                            new TeeWorker(
+                                    mName + " console",
+                                    new FileInputStream(mTeeConsoleOutReader.getFileDescriptor()),
+                                    List.of(
+                                            new FileOutputStream(mPtyFd.getFileDescriptor()),
+                                            new FileOutputStream(
+                                                    mConsoleOutWriter.getFileDescriptor())));
+                    // If the VM is stopped then the tee worker thread would get an EOF or read()
+                    // error which would tear down itself.
+                    mConsoleExecutor.execute(tee);
+                } else if (mConnectVmConsole) {
+                    consoleOutFd = mPtyFd;
+                } else if (mVmOutputCaptured) {
+                    consoleOutFd = mConsoleOutWriter;
+                }
+
+                ParcelFileDescriptor consoleInFd = null;
+                if (mConnectVmConsole) {
+                    consoleInFd = mPtyFd;
+                } else if (mVmConsoleInputSupported) {
+                    consoleInFd = mConsoleInReader;
+                }
+
                 VirtualMachineConfig vmConfig = getConfig();
                 android.system.virtualizationservice.VirtualMachineConfig vmConfigParcel =
                         vmConfig.getCustomImageConfig() != null
@@ -1143,8 +1211,7 @@
                                 : createVirtualMachineConfigForAppFrom(vmConfig, service);
 
                 mVirtualMachine =
-                        service.createVm(
-                                vmConfigParcel, mConsoleOutWriter, mConsoleInReader, mLogWriter);
+                        service.createVm(vmConfigParcel, consoleOutFd, consoleInFd, mLogWriter);
                 mVirtualMachine.registerCallback(new CallbackTranslator(service));
                 mContext.registerComponentCallbacks(mMemoryManagementCallbacks);
                 mVirtualMachine.start();
@@ -1203,15 +1270,81 @@
     private void createVmInputPipes() throws VirtualMachineException {
         try {
             if (mConsoleInReader == null || mConsoleInWriter == null) {
-                ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
-                mConsoleInReader = pipe[0];
-                mConsoleInWriter = pipe[1];
+                if (mConnectVmConsole) {
+                    // If we are enabling input pipes AND the host console, then we should just use
+                    // the host pty peer end as the console write end.
+                    createPtyConsole();
+                    mConsoleInReader = mPtyFd.dup();
+                    mConsoleInWriter = mPtsFd.dup();
+                } else {
+                    ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
+                    mConsoleInReader = pipe[0];
+                    mConsoleInWriter = pipe[1];
+                }
             }
         } catch (IOException e) {
             throw new VirtualMachineException("Failed to create input stream for VM", e);
         }
     }
 
+    @FunctionalInterface
+    private static interface OpenPtyCallback {
+        public void apply(FileDescriptor mfd, FileDescriptor sfd, byte[] name);
+    }
+
+    // Opens a pty and set the master end to raw mode and O_NONBLOCK.
+    private static native void nativeOpenPtyRawNonblock(OpenPtyCallback resultCallback)
+            throws IOException;
+
+    @GuardedBy("mLock")
+    private void createPtyConsole() throws VirtualMachineException {
+        if (mPtyFd != null && mPtsFd != null) {
+            return;
+        }
+        List<FileDescriptor> fd = new ArrayList<>(2);
+        StringBuilder nameBuilder = new StringBuilder();
+        try {
+            try {
+                nativeOpenPtyRawNonblock(
+                        (FileDescriptor mfd, FileDescriptor sfd, byte[] ptsName) -> {
+                            fd.add(mfd);
+                            fd.add(sfd);
+                            nameBuilder.append(new String(ptsName, StandardCharsets.UTF_8));
+                        });
+            } catch (Exception e) {
+                fd.forEach(IoUtils::closeQuietly);
+                throw e;
+            }
+        } catch (IOException e) {
+            throw new VirtualMachineException(
+                    "Failed to create host console to connect to the VM console", e);
+        }
+        mPtyFd = new ParcelFileDescriptor(fd.get(0));
+        mPtsFd = new ParcelFileDescriptor(fd.get(1));
+        mPtsName = nameBuilder.toString();
+        Log.d(TAG, "Serial console device: " + mPtsName);
+    }
+
+    /**
+     * Returns the name of the peer end (ptsname) of the host console. The host console is only
+     * available if the {@link VirtualMachineConfig} specifies that a host console should
+     * {@linkplain VirtualMachineConfig#isConnectVmConsole connect} to the VM console.
+     *
+     * @throws VirtualMachineException if the host pseudoterminal could not be created, or
+     *     connecting to the VM console is not enabled.
+     * @hide
+     */
+    @NonNull
+    public String getHostConsoleName() throws VirtualMachineException {
+        if (!mConnectVmConsole) {
+            throw new VirtualMachineException("Host console is not enabled");
+        }
+        synchronized (mLock) {
+            createPtyConsole();
+            return mPtsName;
+        }
+    }
+
     /**
      * Returns the stream object representing the console output from the virtual machine. The
      * console output is only available if the {@link VirtualMachineConfig} specifies that it should
@@ -1811,4 +1944,61 @@
             }
         }
     }
+
+    /**
+     * Duplicates {@code InputStream} data to multiple {@code OutputStream}. Like the "tee" command.
+     *
+     * <p>Supports non-blocking writes to the output streams by ignoring EAGAIN error.
+     */
+    private static class TeeWorker implements Runnable {
+        private final String mName;
+        private final InputStream mIn;
+        private final List<OutputStream> mOuts;
+
+        TeeWorker(String name, InputStream in, Collection<OutputStream> outs) {
+            mName = name;
+            mIn = in;
+            mOuts = new ArrayList<>(outs);
+        }
+
+        @Override
+        public void run() {
+            byte[] buffer = new byte[2048];
+            try {
+                while (!Thread.interrupted()) {
+                    int len = mIn.read(buffer);
+                    if (len < 0) {
+                        break;
+                    }
+                    for (OutputStream out : mOuts) {
+                        try {
+                            out.write(buffer, 0, len);
+                        } catch (IOException e) {
+                            // EAGAIN is expected because the file description has O_NONBLOCK flag.
+                            if (!isErrnoError(e, OsConstants.EAGAIN)) {
+                                throw e;
+                            }
+                        }
+                    }
+                }
+            } catch (Exception e) {
+                Log.e(TAG, "Tee " + mName, e);
+            }
+        }
+
+        private static ErrnoException asErrnoException(Throwable e) {
+            if (e instanceof ErrnoException) {
+                return (ErrnoException) e;
+            } else if (e instanceof IOException) {
+                // Try to unwrap ErrnoException#rethrowAsIOException()
+                return asErrnoException(e.getCause());
+            }
+            return null;
+        }
+
+        private static boolean isErrnoError(Exception e, int expectedValue) {
+            ErrnoException errno = asErrnoException(e);
+            return errno != null && errno.errno == expectedValue;
+        }
+    }
 }
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java b/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
index 1b915cd..66d0f4b 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -48,7 +48,6 @@
 
 import com.android.system.virtualmachine.flags.Flags;
 
-
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
@@ -94,6 +93,7 @@
     private static final String KEY_ENCRYPTED_STORAGE_BYTES = "encryptedStorageBytes";
     private static final String KEY_VM_OUTPUT_CAPTURED = "vmOutputCaptured";
     private static final String KEY_VM_CONSOLE_INPUT_SUPPORTED = "vmConsoleInputSupported";
+    private static final String KEY_CONNECT_VM_CONSOLE = "connectVmConsole";
     private static final String KEY_VENDOR_DISK_IMAGE_PATH = "vendorDiskImagePath";
     private static final String KEY_OS = "os";
     private static final String KEY_EXTRA_APKS = "extraApks";
@@ -193,6 +193,9 @@
     /** Whether the app can write console input to the VM */
     private final boolean mVmConsoleInputSupported;
 
+    /** Whether to connect the VM console to a host console. */
+    private final boolean mConnectVmConsole;
+
     @Nullable private final File mVendorDiskImage;
 
     /** OS name of the VM using payload binaries. */
@@ -229,6 +232,7 @@
             long encryptedStorageBytes,
             boolean vmOutputCaptured,
             boolean vmConsoleInputSupported,
+            boolean connectVmConsole,
             @Nullable File vendorDiskImage,
             @NonNull @OsName String os) {
         // This is only called from Builder.build(); the builder handles parameter validation.
@@ -249,6 +253,7 @@
         mEncryptedStorageBytes = encryptedStorageBytes;
         mVmOutputCaptured = vmOutputCaptured;
         mVmConsoleInputSupported = vmConsoleInputSupported;
+        mConnectVmConsole = connectVmConsole;
         mVendorDiskImage = vendorDiskImage;
         mOs = os;
     }
@@ -331,6 +336,7 @@
         }
         builder.setVmOutputCaptured(b.getBoolean(KEY_VM_OUTPUT_CAPTURED));
         builder.setVmConsoleInputSupported(b.getBoolean(KEY_VM_CONSOLE_INPUT_SUPPORTED));
+        builder.setConnectVmConsole(b.getBoolean(KEY_CONNECT_VM_CONSOLE));
 
         String vendorDiskImagePath = b.getString(KEY_VENDOR_DISK_IMAGE_PATH);
         if (vendorDiskImagePath != null) {
@@ -384,6 +390,7 @@
         }
         b.putBoolean(KEY_VM_OUTPUT_CAPTURED, mVmOutputCaptured);
         b.putBoolean(KEY_VM_CONSOLE_INPUT_SUPPORTED, mVmConsoleInputSupported);
+        b.putBoolean(KEY_CONNECT_VM_CONSOLE, mConnectVmConsole);
         if (mVendorDiskImage != null) {
             b.putString(KEY_VENDOR_DISK_IMAGE_PATH, mVendorDiskImage.getAbsolutePath());
         }
@@ -544,6 +551,16 @@
     }
 
     /**
+     * Returns whether to connect the VM console to a host console.
+     *
+     * @see Builder#setConnectVmConsole
+     * @hide
+     */
+    public boolean isConnectVmConsole() {
+        return mConnectVmConsole;
+    }
+
+    /**
      * Returns the OS of the VM.
      *
      * @see Builder#setOs
@@ -577,6 +594,7 @@
                 && this.mEncryptedStorageBytes == other.mEncryptedStorageBytes
                 && this.mVmOutputCaptured == other.mVmOutputCaptured
                 && this.mVmConsoleInputSupported == other.mVmConsoleInputSupported
+                && this.mConnectVmConsole == other.mConnectVmConsole
                 && (this.mVendorDiskImage == null) == (other.mVendorDiskImage == null)
                 && Objects.equals(this.mPayloadConfigPath, other.mPayloadConfigPath)
                 && Objects.equals(this.mPayloadBinaryName, other.mPayloadBinaryName)
@@ -789,6 +807,7 @@
         private long mEncryptedStorageBytes;
         private boolean mVmOutputCaptured = false;
         private boolean mVmConsoleInputSupported = false;
+        private boolean mConnectVmConsole = false;
         @Nullable private File mVendorDiskImage;
         @NonNull @OsName private String mOs = DEFAULT_OS;
 
@@ -862,6 +881,11 @@
                 throw new IllegalStateException("debug level must be FULL to use console input");
             }
 
+            if (mConnectVmConsole && mDebugLevel != DEBUG_LEVEL_FULL) {
+                throw new IllegalStateException(
+                        "debug level must be FULL to connect to the console");
+            }
+
             return new VirtualMachineConfig(
                     packageName,
                     apkPath,
@@ -876,6 +900,7 @@
                     mEncryptedStorageBytes,
                     mVmOutputCaptured,
                     mVmConsoleInputSupported,
+                    mConnectVmConsole,
                     mVendorDiskImage,
                     mOs);
         }
@@ -1125,6 +1150,23 @@
         }
 
         /**
+         * Sets whether to connect the VM console to a host console. Default is {@code false}.
+         *
+         * <p>Setting this as {@code true} will allow the shell to directly communicate with the VM
+         * console through the connected host console.
+         *
+         * <p>The {@linkplain #setDebugLevel debug level} must be {@link #DEBUG_LEVEL_FULL} to be
+         * set as true.
+         *
+         * @hide
+         */
+        @NonNull
+        public Builder setConnectVmConsole(boolean supported) {
+            mConnectVmConsole = supported;
+            return this;
+        }
+
+        /**
          * Sets the path to the disk image with vendor-specific modules.
          *
          * @hide
diff --git a/java/jni/android_system_virtualmachine_VirtualMachine.cpp b/java/jni/android_system_virtualmachine_VirtualMachine.cpp
index b3354cc..ed102bf 100644
--- a/java/jni/android_system_virtualmachine_VirtualMachine.cpp
+++ b/java/jni/android_system_virtualmachine_VirtualMachine.cpp
@@ -17,16 +17,36 @@
 #define LOG_TAG "VirtualMachine"
 
 #include <aidl/android/system/virtualizationservice/IVirtualMachine.h>
+#include <android-base/scopeguard.h>
+#include <android-base/strings.h>
 #include <android/binder_auto_utils.h>
 #include <android/binder_ibinder_jni.h>
+#include <fcntl.h>
 #include <jni.h>
 #include <log/log.h>
+#include <nativehelper/JNIHelp.h>
+#include <nativehelper/JNIPlatformHelp.h>
+#include <nativehelper/ScopedLocalRef.h>
+#include <pty.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <termios.h>
+#include <unistd.h>
 
 #include <binder_rpc_unstable.hpp>
+#include <string>
 #include <tuple>
 
 #include "common.h"
 
+namespace {
+
+void throwIOException(JNIEnv *env, const std::string &msg) {
+    jniThrowException(env, "java/io/IOException", msg.c_str());
+}
+
+} // namespace
+
 extern "C" JNIEXPORT jobject JNICALL
 Java_android_system_virtualmachine_VirtualMachine_nativeConnectToVsockServer(
         JNIEnv* env, [[maybe_unused]] jclass clazz, jobject vmBinder, jint port) {
@@ -65,3 +85,72 @@
     auto client = ARpcSession_setupPreconnectedClient(session.get(), requestFunc, &args);
     return AIBinder_toJavaBinder(env, client);
 }
+
+extern "C" JNIEXPORT void JNICALL
+Java_android_system_virtualmachine_VirtualMachine_nativeOpenPtyRawNonblock(
+        JNIEnv *env, [[maybe_unused]] jclass clazz, jobject resultCallback) {
+    int pm, ps;
+    // man openpty says: "Nobody knows how much space should be reserved for name."
+    // but on modern Linux the format of the pts name is always `/dev/pts/[0-9]+`
+    // Realistically speaking, a buffer of 32 bytes leaves us with 22 digits for the pts number,
+    // which should be more than enough.
+    // NOTE: bionic implements openpty() with internal name buffer of size 32, musl 20.
+    char name[32];
+    if (openpty(&pm, &ps, name, nullptr, nullptr)) {
+        throwIOException(env, "openpty(): " + android::base::ErrnoNumberAsString(errno));
+        return;
+    }
+    fcntl(pm, F_SETFD, FD_CLOEXEC);
+    fcntl(ps, F_SETFD, FD_CLOEXEC);
+    name[sizeof(name) - 1] = '\0';
+    // Set world RW so adb shell can talk to the pts.
+    chmod(name, 0666);
+
+    if (int flags = fcntl(pm, F_GETFL, 0); flags < 0) {
+        throwIOException(env, "fcntl(F_GETFL): " + android::base::ErrnoNumberAsString(errno));
+        return;
+    } else if (fcntl(pm, F_SETFL, flags | O_NONBLOCK) < 0) {
+        throwIOException(env, "fcntl(F_SETFL): " + android::base::ErrnoNumberAsString(errno));
+        return;
+    }
+
+    android::base::ScopeGuard cleanup_handler([=] {
+        close(ps);
+        close(pm);
+    });
+
+    struct termios tio;
+    if (tcgetattr(pm, &tio)) {
+        throwIOException(env, "tcgetattr(): " + android::base::ErrnoNumberAsString(errno));
+        return;
+    }
+    cfmakeraw(&tio);
+    if (tcsetattr(pm, TCSANOW, &tio)) {
+        throwIOException(env, "tcsetattr(): " + android::base::ErrnoNumberAsString(errno));
+        return;
+    }
+
+    jobject mfd = jniCreateFileDescriptor(env, pm);
+    if (mfd == nullptr) {
+        return;
+    }
+    jobject sfd = jniCreateFileDescriptor(env, ps);
+    if (sfd == nullptr) {
+        return;
+    }
+    size_t len = strlen(name);
+    ScopedLocalRef<jbyteArray> ptsName(env, env->NewByteArray(len));
+    if (ptsName.get() != nullptr) {
+        env->SetByteArrayRegion(ptsName.get(), 0, len, (jbyte *)name);
+    }
+    ScopedLocalRef<jclass> callback_class(env, env->GetObjectClass(resultCallback));
+    jmethodID mid = env->GetMethodID(callback_class.get(), "apply",
+                                     "(Ljava/io/FileDescriptor;Ljava/io/FileDescriptor;[B)V");
+    if (mid == nullptr) {
+        return;
+    }
+
+    env->CallVoidMethod(resultCallback, mid, mfd, sfd, ptsName.get());
+    // FD ownership is transferred to the callback, reset the auto-close hander.
+    cleanup_handler.Disable();
+}
diff --git a/virtualizationservice/vmnic/Android.bp b/virtualizationservice/vmnic/Android.bp
index 784c648..247be85 100644
--- a/virtualizationservice/vmnic/Android.bp
+++ b/virtualizationservice/vmnic/Android.bp
@@ -14,7 +14,9 @@
         "libandroid_logger",
         "libanyhow",
         "libbinder_rs",
+        "liblibc",
         "liblog_rust",
+        "libnix",
     ],
     apex_available: ["com.android.virt"],
 }
diff --git a/virtualizationservice/vmnic/src/aidl.rs b/virtualizationservice/vmnic/src/aidl.rs
index 6443258..ef1fda9 100644
--- a/virtualizationservice/vmnic/src/aidl.rs
+++ b/virtualizationservice/vmnic/src/aidl.rs
@@ -14,10 +14,64 @@
 
 //! Implementation of the AIDL interface of Vmnic.
 
-use anyhow::anyhow;
+use anyhow::{anyhow, Context, Result};
 use android_system_virtualizationservice_internal::aidl::android::system::virtualizationservice_internal::IVmnic::IVmnic;
-use binder::{self, ExceptionCode, Interface, IntoBinderResult, ParcelFileDescriptor};
+use binder::{self, Interface, IntoBinderResult, ParcelFileDescriptor};
+use libc::{c_char, c_int, c_short, ifreq, IFF_NO_PI, IFF_TAP, IFF_UP, IFNAMSIZ};
 use log::info;
+use nix::{ioctl_write_int_bad, ioctl_write_ptr_bad};
+use nix::sys::ioctl::ioctl_num_type;
+use nix::sys::socket::{socket, AddressFamily, SockFlag, SockType};
+use std::ffi::CString;
+use std::fs::File;
+use std::os::fd::{AsRawFd, RawFd};
+use std::slice::from_raw_parts;
+
+const TUNSETIFF: ioctl_num_type = 0x400454ca;
+const TUNSETPERSIST: ioctl_num_type = 0x400454cb;
+const SIOCGIFFLAGS: ioctl_num_type = 0x00008913;
+const SIOCSIFFLAGS: ioctl_num_type = 0x00008914;
+
+ioctl_write_ptr_bad!(ioctl_tunsetiff, TUNSETIFF, ifreq);
+ioctl_write_int_bad!(ioctl_tunsetpersist, TUNSETPERSIST);
+ioctl_write_ptr_bad!(ioctl_siocgifflags, SIOCGIFFLAGS, ifreq);
+ioctl_write_ptr_bad!(ioctl_siocsifflags, SIOCSIFFLAGS, ifreq);
+
+fn validate_ifname(ifname: &[c_char]) -> Result<()> {
+    if ifname.len() >= IFNAMSIZ {
+        return Err(anyhow!(format!("Interface name is too long")));
+    }
+    Ok(())
+}
+
+fn create_tap_interface(fd: RawFd, ifname: &[c_char]) -> Result<()> {
+    // SAFETY: All-zero is a valid value for the ifreq type.
+    let mut ifr: ifreq = unsafe { std::mem::zeroed() };
+    ifr.ifr_ifru.ifru_flags = (IFF_TAP | IFF_NO_PI) as c_short;
+    ifr.ifr_name[..ifname.len()].copy_from_slice(ifname);
+    // SAFETY: `ioctl` is copied into the kernel. It modifies the state in the kernel, not the
+    // state of this process in any way.
+    unsafe { ioctl_tunsetiff(fd, &ifr) }.context("Failed to ioctl TUNSETIFF")?;
+    // SAFETY: `ioctl` is copied into the kernel. It modifies the state in the kernel, not the
+    // state of this process in any way.
+    unsafe { ioctl_tunsetpersist(fd, 1) }.context("Failed to ioctl TUNSETPERSIST")?;
+    Ok(())
+}
+
+fn bring_up_interface(sockfd: c_int, ifname: &[c_char]) -> Result<()> {
+    // SAFETY: All-zero is a valid value for the ifreq type.
+    let mut ifr: ifreq = unsafe { std::mem::zeroed() };
+    ifr.ifr_name[..ifname.len()].copy_from_slice(ifname);
+    // SAFETY: `ioctl` is copied into the kernel. It modifies the state in the kernel, not the
+    // state of this process in any way.
+    unsafe { ioctl_siocgifflags(sockfd, &ifr) }.context("Failed to ioctl SIOCGIFFLAGS")?;
+    // SAFETY: After calling SIOCGIFFLAGS, ifr_ifru holds ifru_flags in its union field.
+    unsafe { ifr.ifr_ifru.ifru_flags |= IFF_UP as c_short };
+    // SAFETY: `ioctl` is copied into the kernel. It modifies the state in the kernel, not the
+    // state of this process in any way.
+    unsafe { ioctl_siocsifflags(sockfd, &ifr) }.context("Failed to ioctl SIOCGIFFLAGS")?;
+    Ok(())
+}
 
 #[derive(Debug, Default)]
 pub struct Vmnic {}
@@ -32,10 +86,34 @@
 
 impl IVmnic for Vmnic {
     fn createTapInterface(&self, iface_name_suffix: &str) -> binder::Result<ParcelFileDescriptor> {
-        let ifname = format!("avf_tap_{iface_name_suffix}");
-        info!("Creating TAP interface {}", ifname);
+        let ifname = CString::new(format!("avf_tap_{iface_name_suffix}"))
+            .context(format!(
+                "Failed to construct TAP interface name as CString: avf_tap_{iface_name_suffix}"
+            ))
+            .or_service_specific_exception(-1)?;
+        let ifname_bytes = ifname.as_bytes_with_nul();
+        // SAFETY: Converting from &[u8] into &[c_char].
+        let ifname_bytes =
+            unsafe { from_raw_parts(ifname_bytes.as_ptr().cast::<c_char>(), ifname_bytes.len()) };
+        validate_ifname(ifname_bytes)
+            .context(format!("Invalid interface name: {ifname:#?}"))
+            .or_service_specific_exception(-1)?;
 
-        Err(anyhow!("Creating TAP network interface is not supported yet"))
-            .or_binder_exception(ExceptionCode::UNSUPPORTED_OPERATION)
+        let tunfd = File::open("/dev/tun")
+            .context("Failed to open /dev/tun")
+            .or_service_specific_exception(-1)?;
+        create_tap_interface(tunfd.as_raw_fd(), ifname_bytes)
+            .context(format!("Failed to create TAP interface: {ifname:#?}"))
+            .or_service_specific_exception(-1)?;
+
+        let sock = socket(AddressFamily::Inet, SockType::Datagram, SockFlag::empty(), None)
+            .context("Failed to create socket")
+            .or_service_specific_exception(-1)?;
+        bring_up_interface(sock.as_raw_fd(), ifname_bytes)
+            .context(format!("Failed to bring up TAP interface: {ifname:#?}"))
+            .or_service_specific_exception(-1)?;
+
+        info!("Created TAP network interface: {ifname:#?}");
+        Ok(ParcelFileDescriptor::new(tunfd))
     }
 }
diff --git a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
index 10f8bf6..521e2f1 100644
--- a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
+++ b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
@@ -19,40 +19,43 @@
 import static android.system.virtualmachine.VirtualMachineConfig.CPU_TOPOLOGY_MATCH_HOST;
 
 import android.app.Activity;
+import android.crosvm.ICrosvmAndroidDisplayService;
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.os.RemoteException;
 import android.os.ServiceManager;
-import android.crosvm.ICrosvmAndroidDisplayService;
 import android.system.virtualizationservice_internal.IVirtualizationServiceInternal;
-import android.system.virtualmachine.VirtualMachineCustomImageConfig;
-import android.system.virtualmachine.VirtualMachineCustomImageConfig.DisplayConfig;
-import android.util.DisplayMetrics;
-import android.util.Log;
 import android.system.virtualmachine.VirtualMachine;
 import android.system.virtualmachine.VirtualMachineCallback;
 import android.system.virtualmachine.VirtualMachineConfig;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig.DisplayConfig;
 import android.system.virtualmachine.VirtualMachineException;
 import android.system.virtualmachine.VirtualMachineManager;
+import android.util.DisplayMetrics;
+import android.util.Log;
 import android.view.Display;
 import android.view.InputDevice;
+import android.view.KeyEvent;
 import android.view.SurfaceHolder;
 import android.view.SurfaceView;
-import android.view.KeyEvent;
 import android.view.View;
-import android.view.WindowManager;
 import android.view.WindowInsets;
 import android.view.WindowInsetsController;
+import android.view.WindowManager;
 import android.view.WindowMetrics;
 
+
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
+import java.io.BufferedOutputStream;
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.io.OutputStream;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.Arrays;
@@ -63,7 +66,7 @@
     private static final String TAG = "VmLauncherApp";
     private static final String VM_NAME = "my_custom_vm";
     private static final boolean DEBUG = true;
-    private final ExecutorService mExecutorService = Executors.newFixedThreadPool(4);
+    private ExecutorService mExecutorService;
     private VirtualMachine mVirtualMachine;
 
     private VirtualMachineConfig createVirtualMachineConfig(String jsonPath) {
@@ -75,6 +78,7 @@
         if (DEBUG) {
             configBuilder.setDebugLevel(VirtualMachineConfig.DEBUG_LEVEL_FULL);
             configBuilder.setVmOutputCaptured(true);
+            configBuilder.setConnectVmConsole(true);
         }
         VirtualMachineCustomImageConfig.Builder customImageConfigBuilder =
                 new VirtualMachineCustomImageConfig.Builder();
@@ -160,6 +164,7 @@
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        mExecutorService = Executors.newCachedThreadPool();
         try {
             // To ensure that the previous display service is removed.
             IVirtualizationServiceInternal.Stub.asInterface(
@@ -239,10 +244,13 @@
             if (DEBUG) {
                 InputStream console = mVirtualMachine.getConsoleOutput();
                 InputStream log = mVirtualMachine.getLogOutput();
-                mExecutorService.execute(new Reader("console", console));
+                OutputStream consoleLogFile =
+                        new LineBufferedOutputStream(
+                                getApplicationContext().openFileOutput("console.log", 0));
+                mExecutorService.execute(new CopyStreamTask("console", console, consoleLogFile));
                 mExecutorService.execute(new Reader("log", log));
             }
-        } catch (VirtualMachineException e) {
+        } catch (VirtualMachineException | IOException e) {
             throw new RuntimeException(e);
         }
 
@@ -305,6 +313,15 @@
     }
 
     @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        if (mExecutorService != null) {
+            mExecutorService.shutdownNow();
+        }
+        Log.d(TAG, "destroyed");
+    }
+
+    @Override
     public void onWindowFocusChanged(boolean hasFocus) {
         super.onWindowFocusChanged(hasFocus);
         if (hasFocus) {
@@ -359,4 +376,49 @@
             }
         }
     }
+
+    private static class CopyStreamTask implements Runnable {
+        private final String mName;
+        private final InputStream mIn;
+        private final OutputStream mOut;
+
+        CopyStreamTask(String name, InputStream in, OutputStream out) {
+            mName = name;
+            mIn = in;
+            mOut = out;
+        }
+
+        @Override
+        public void run() {
+            try {
+                byte[] buffer = new byte[2048];
+                while (!Thread.interrupted()) {
+                    int len = mIn.read(buffer);
+                    if (len < 0) {
+                        break;
+                    }
+                    mOut.write(buffer, 0, len);
+                }
+            } catch (Exception e) {
+                Log.e(TAG, "Exception while posting " + mName, e);
+            }
+        }
+    }
+
+    private static class LineBufferedOutputStream extends BufferedOutputStream {
+        LineBufferedOutputStream(OutputStream out) {
+            super(out);
+        }
+
+        @Override
+        public void write(byte[] buf, int off, int len) throws IOException {
+            super.write(buf, off, len);
+            for (int i = 0; i < len; ++i) {
+                if (buf[off + i] == '\n') {
+                    flush();
+                    break;
+                }
+            }
+        }
+    }
 }