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;
+ }
+ }
+ }
+ }
}