Merge "Validate collecting guest_time in pkvm hypervisor only" into main
diff --git a/OWNERS b/OWNERS
index e560cec..40c709f 100644
--- a/OWNERS
+++ b/OWNERS
@@ -4,7 +4,6 @@
#
# If you are not a member of the project please send review requests
# to one of those listed below.
-dbrazdil@google.com
jiyong@google.com
smoreland@google.com
willdeacon@google.com
@@ -13,6 +12,7 @@
alanstokes@google.com
aliceywang@google.com
inseob@google.com
+ioffe@google.com
jaewan@google.com
jakobvukalovic@google.com
jeffv@google.com
diff --git a/apex/Android.bp b/apex/Android.bp
index 99b2dee..43819dc 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -47,7 +47,7 @@
"release_avf_enable_device_assignment",
"release_avf_enable_llpvm_changes",
"release_avf_enable_network",
- "release_avf_enable_remote_attestation",
+ "avf_remote_attestation_enabled",
"release_avf_enable_vendor_modules",
"release_avf_enable_virt_cpufreq",
"release_avf_support_custom_vm_with_paravirtualized_devices",
@@ -204,7 +204,7 @@
},
},
},
- release_avf_enable_remote_attestation: {
+ avf_remote_attestation_enabled: {
vintf_fragments: [
"virtualizationservice.xml",
],
@@ -235,7 +235,7 @@
config_namespace: "ANDROID",
bool_variables: [
"release_avf_enable_llpvm_changes",
- "release_avf_enable_remote_attestation",
+ "avf_remote_attestation_enabled",
],
properties: ["srcs"],
}
@@ -247,7 +247,7 @@
release_avf_enable_llpvm_changes: {
srcs: ["virtualizationservice.rc.llpvm"],
},
- release_avf_enable_remote_attestation: {
+ avf_remote_attestation_enabled: {
srcs: ["virtualizationservice.rc.ra"],
},
},
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/compos/apk/assets/vm_config.json b/compos/apk/assets/vm_config.json
index 1f5cdba..28e0f07 100644
--- a/compos/apk/assets/vm_config.json
+++ b/compos/apk/assets/vm_config.json
@@ -27,5 +27,6 @@
}
],
"export_tombstones": true,
- "enable_authfs": true
+ "enable_authfs": true,
+ "hugepages": true
}
diff --git a/compos/apk/assets/vm_config_staged.json b/compos/apk/assets/vm_config_staged.json
index 37b1d7a..afc3767 100644
--- a/compos/apk/assets/vm_config_staged.json
+++ b/compos/apk/assets/vm_config_staged.json
@@ -28,5 +28,6 @@
}
],
"export_tombstones": true,
- "enable_authfs": true
+ "enable_authfs": true,
+ "hugepages": true
}
diff --git a/compos/apk/assets/vm_config_system_ext.json b/compos/apk/assets/vm_config_system_ext.json
index 1ef43f0..730f592 100644
--- a/compos/apk/assets/vm_config_system_ext.json
+++ b/compos/apk/assets/vm_config_system_ext.json
@@ -30,5 +30,6 @@
}
],
"export_tombstones": true,
- "enable_authfs": true
+ "enable_authfs": true,
+ "hugepages": true
}
diff --git a/compos/apk/assets/vm_config_system_ext_staged.json b/compos/apk/assets/vm_config_system_ext_staged.json
index 9103a9e..6d91aa2 100644
--- a/compos/apk/assets/vm_config_system_ext_staged.json
+++ b/compos/apk/assets/vm_config_system_ext_staged.json
@@ -31,5 +31,6 @@
}
],
"export_tombstones": true,
- "enable_authfs": true
+ "enable_authfs": true,
+ "hugepages": true
}
diff --git a/docs/custom_vm.md b/docs/custom_vm.md
index 0825f06..5511758 100644
--- a/docs/custom_vm.md
+++ b/docs/custom_vm.md
@@ -209,6 +209,14 @@
$ adb unroot
```
+If virt apex is Google-signed, you need to enable the app and grant the
+permission to the app.
+```
+$ adb root
+$ adb shell pm enable com.google.android.virtualization.vmlauncher/com.android.virtualization.vmlauncher.MainActivity
+$ adb shell pm grant com.google.android.virtualization.vmlauncher android.permission.USE_CUSTOM_VIRTUAL_MACHINE
+$ adb unroot
+```
Then execute the below to set up the network. In the future, this step won't be necessary.
```
diff --git a/docs/vm_remote_attestation.md b/docs/vm_remote_attestation.md
index 835dcac..3483351 100644
--- a/docs/vm_remote_attestation.md
+++ b/docs/vm_remote_attestation.md
@@ -106,3 +106,18 @@
normal mode.
- The `vmComponents` field contains a list of all the APKs and apexes loaded
by the pVM.
+
+## To Support It
+
+VM remote attestation is a strongly recommended feature from Android V. To support
+it, you only need to provide a valid VM DICE chain satisfying the following
+requirements:
+
+- The DICE chain must have a UDS-rooted public key registered at the RKP factory.
+- The DICE chain should have RKP VM markers that help identify RKP VM as required
+ by the [remote provisioning HAL][rkp-hal-markers].
+
+The feature is enabled by default. To disable it, you can set
+`PRODUCT_AVF_REMOTE_ATTESTATION_DISABLED` to true in your Makefile.
+
+[rkp-hal-markers]: https://android.googlesource.com/platform/hardware/interfaces/+/main/security/rkp/README.md#hal
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/libs/android_display_backend/aidl/android/crosvm/ICrosvmAndroidDisplayService.aidl b/libs/android_display_backend/aidl/android/crosvm/ICrosvmAndroidDisplayService.aidl
index c7bfc80..e42cdd1 100644
--- a/libs/android_display_backend/aidl/android/crosvm/ICrosvmAndroidDisplayService.aidl
+++ b/libs/android_display_backend/aidl/android/crosvm/ICrosvmAndroidDisplayService.aidl
@@ -16,6 +16,7 @@
package android.crosvm;
+import android.os.ParcelFileDescriptor;
import android.view.Surface;
/**
@@ -23,7 +24,7 @@
* display.
*/
interface ICrosvmAndroidDisplayService {
- void setSurface(inout Surface surface);
-
- void removeSurface();
+ void setSurface(inout Surface surface, boolean forCursor);
+ void setCursorStream(in ParcelFileDescriptor stream);
+ void removeSurface(boolean forCursor);
}
diff --git a/libs/android_display_backend/crosvm_android_display_client.cpp b/libs/android_display_backend/crosvm_android_display_client.cpp
index 66320f3..0557127 100644
--- a/libs/android_display_backend/crosvm_android_display_client.cpp
+++ b/libs/android_display_backend/crosvm_android_display_client.cpp
@@ -35,34 +35,54 @@
DisplayService() = default;
virtual ~DisplayService() = default;
- ndk::ScopedAStatus setSurface(Surface* surface) override {
+ ndk::ScopedAStatus setSurface(Surface* surface, bool forCursor) override {
{
std::lock_guard lk(mSurfaceReadyMutex);
- mSurface = std::make_unique<Surface>(surface->release());
+ if (forCursor) {
+ mCursorSurface = std::make_unique<Surface>(surface->release());
+ } else {
+ mSurface = std::make_unique<Surface>(surface->release());
+ }
}
- mSurfaceReady.notify_one();
+ mSurfaceReady.notify_all();
return ::ndk::ScopedAStatus::ok();
}
- ndk::ScopedAStatus removeSurface() override {
+ ndk::ScopedAStatus removeSurface(bool forCursor) override {
{
std::lock_guard lk(mSurfaceReadyMutex);
- mSurface = nullptr;
+ if (forCursor) {
+ mCursorSurface = nullptr;
+ } else {
+ mSurface = nullptr;
+ }
}
- mSurfaceReady.notify_one();
+ mSurfaceReady.notify_all();
return ::ndk::ScopedAStatus::ok();
}
- Surface* getSurface() {
+ Surface* getSurface(bool forCursor) {
std::unique_lock lk(mSurfaceReadyMutex);
- mSurfaceReady.wait(lk, [this] { return mSurface != nullptr; });
- return mSurface.get();
+ if (forCursor) {
+ mSurfaceReady.wait(lk, [this] { return mCursorSurface != nullptr; });
+ return mCursorSurface.get();
+ } else {
+ mSurfaceReady.wait(lk, [this] { return mSurface != nullptr; });
+ return mSurface.get();
+ }
+ }
+ ndk::ScopedFileDescriptor& getCursorStream() { return mCursorStream; }
+ ndk::ScopedAStatus setCursorStream(const ndk::ScopedFileDescriptor& in_stream) {
+ mCursorStream = ndk::ScopedFileDescriptor(dup(in_stream.get()));
+ return ::ndk::ScopedAStatus::ok();
}
private:
std::condition_variable mSurfaceReady;
std::mutex mSurfaceReadyMutex;
std::unique_ptr<Surface> mSurface;
+ std::unique_ptr<Surface> mCursorSurface;
+ ndk::ScopedFileDescriptor mCursorStream;
};
} // namespace
@@ -130,7 +150,7 @@
}
extern "C" ANativeWindow* create_android_surface(struct AndroidDisplayContext* ctx, uint32_t width,
- uint32_t height) {
+ uint32_t height, bool for_cursor) {
if (ctx->disp_service == nullptr) {
ctx->errorf("Display service was not created");
return nullptr;
@@ -139,7 +159,7 @@
// where the SetScanoutBlob command is handled. Let's use BGRA not BGRX with a hope that we will
// need alpha blending for the cursor surface.
int format = HAL_PIXEL_FORMAT_BGRA_8888;
- ANativeWindow* surface = ctx->disp_service->getSurface()->get(); // this can block
+ ANativeWindow* surface = ctx->disp_service->getSurface(for_cursor)->get(); // this can block
if (ANativeWindow_setBuffersGeometry(surface, width, height, format) != 0) {
ctx->errorf("Failed to set buffer gemoetry");
return nullptr;
@@ -168,6 +188,21 @@
return true;
}
+extern "C" void set_android_surface_position(struct AndroidDisplayContext* ctx, uint32_t x,
+ uint32_t y) {
+ if (ctx->disp_service == nullptr) {
+ ctx->errorf("Display service was not created");
+ return;
+ }
+ auto fd = ctx->disp_service->getCursorStream().get();
+ if (fd == -1) {
+ ctx->errorf("Invalid fd");
+ return;
+ }
+ uint32_t pos[] = {x, y};
+ write(fd, pos, sizeof(pos));
+}
+
extern "C" void post_android_surface_buffer(struct AndroidDisplayContext* ctx,
ANativeWindow* surface) {
if (ANativeWindow_unlockAndPost(surface) != 0) {
diff --git a/pvmfw/Android.bp b/pvmfw/Android.bp
index 37a321d..144e81e 100644
--- a/pvmfw/Android.bp
+++ b/pvmfw/Android.bp
@@ -14,6 +14,7 @@
"libaarch64_paging",
"libbssl_avf_nostd",
"libbssl_sys_nostd",
+ "libcbor_util_nostd",
"libciborium_nostd",
"libciborium_io_nostd",
"libcstr",
@@ -116,9 +117,10 @@
rustlibs: [
"libcbor_util",
"libciborium",
- "libdiced_open_dice_nostd",
+ "libdiced_open_dice",
"libpvmfw_avb_nostd",
"libzerocopy_nostd",
+ "libhex",
],
}
@@ -319,15 +321,22 @@
installable: false,
}
-prebuilt_etc {
+filegroup {
name: "pvmfw_embedded_key",
- src: ":avb_testkey_rsa4096_pub_bin",
- installable: false,
+ srcs: [":avb_testkey_rsa4096"],
+}
+
+genrule {
+ name: "pvmfw_embedded_key_pub_bin",
+ tools: ["avbtool"],
+ srcs: [":pvmfw_embedded_key"],
+ out: ["pvmfw_embedded_key_pub.bin"],
+ cmd: "$(location avbtool) extract_public_key --key $(in) --output $(out)",
}
genrule {
name: "pvmfw_embedded_key_rs",
- srcs: [":pvmfw_embedded_key"],
+ srcs: [":pvmfw_embedded_key_pub_bin"],
out: ["lib.rs"],
cmd: "(" +
" echo '#![no_std]';" +
diff --git a/pvmfw/platform.dts b/pvmfw/platform.dts
index 68acf13..99ecf8f 100644
--- a/pvmfw/platform.dts
+++ b/pvmfw/platform.dts
@@ -308,11 +308,11 @@
GIC_PPI 0xa IRQ_TYPE_LEVEL_LOW>;
};
- uart@2e8 {
+ uart@3f8 {
compatible = "ns16550a";
- reg = <0x00 0x2e8 0x00 0x8>;
+ reg = <0x00 0x3f8 0x00 0x8>;
clock-frequency = <0x1c2000>;
- interrupts = <GIC_SPI 2 IRQ_TYPE_EDGE_RISING>;
+ interrupts = <GIC_SPI 0 IRQ_TYPE_EDGE_RISING>;
};
uart@2f8 {
@@ -329,11 +329,11 @@
interrupts = <GIC_SPI 0 IRQ_TYPE_EDGE_RISING>;
};
- uart@3f8 {
+ uart@2e8 {
compatible = "ns16550a";
- reg = <0x00 0x3f8 0x00 0x8>;
+ reg = <0x00 0x2e8 0x00 0x8>;
clock-frequency = <0x1c2000>;
- interrupts = <GIC_SPI 0 IRQ_TYPE_EDGE_RISING>;
+ interrupts = <GIC_SPI 2 IRQ_TYPE_EDGE_RISING>;
};
psci {
diff --git a/pvmfw/src/bcc.rs b/pvmfw/src/bcc.rs
index f56e62b..7a13da7 100644
--- a/pvmfw/src/bcc.rs
+++ b/pvmfw/src/bcc.rs
@@ -27,10 +27,9 @@
type Result<T> = core::result::Result<T, BccError>;
pub enum BccError {
- CborDecodeError(ciborium::de::Error<ciborium_io::EndOfFile>),
- CborEncodeError(ciborium::ser::Error<core::convert::Infallible>),
+ CborDecodeError,
+ CborEncodeError,
DiceError(diced_open_dice::DiceError),
- ExtraneousBytes,
MalformedBcc(&'static str),
MissingBcc,
}
@@ -38,10 +37,9 @@
impl fmt::Display for BccError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
- Self::CborDecodeError(e) => write!(f, "Error parsing BCC CBOR: {e:?}"),
- Self::CborEncodeError(e) => write!(f, "Error encoding BCC CBOR: {e:?}"),
+ Self::CborDecodeError => write!(f, "Error parsing BCC CBOR"),
+ Self::CborEncodeError => write!(f, "Error encoding BCC CBOR"),
Self::DiceError(e) => write!(f, "Dice error: {e:?}"),
- Self::ExtraneousBytes => write!(f, "Unexpected trailing data in BCC"),
Self::MalformedBcc(s) => {
write!(f, "BCC does not have the expected CBOR structure: {s}")
}
@@ -65,7 +63,7 @@
// }
let bcc_handover: Vec<(Value, Value)> =
vec![(1.into(), cdi_attest.as_slice().into()), (2.into(), cdi_seal.as_slice().into())];
- value_to_bytes(&bcc_handover.into())
+ cbor_util::serialize(&bcc_handover).map_err(|_| BccError::CborEncodeError)
}
fn taint_cdi(cdi: &Cdi, info: &str) -> Result<Cdi> {
@@ -100,7 +98,8 @@
// We don't attempt to fully validate the BCC (e.g. we don't check the signatures) - we
// have to trust our loader. But if it's invalid CBOR or otherwise clearly ill-formed,
// something is very wrong, so we fail.
- let bcc_cbor = value_from_bytes(received_bcc)?;
+ let bcc_cbor =
+ cbor_util::deserialize(received_bcc).map_err(|_| BccError::CborDecodeError)?;
// Bcc = [
// PubKeyEd25519 / PubKeyECDSA256, // DK_pub
@@ -159,7 +158,7 @@
// ]
let payload =
self.payload_bytes().ok_or(BccError::MalformedBcc("Invalid payload in BccEntry"))?;
- let payload = value_from_bytes(payload)?;
+ let payload = cbor_util::deserialize(payload).map_err(|_| BccError::CborDecodeError)?;
trace!("Bcc payload: {payload:?}");
Ok(BccPayload(payload))
}
@@ -215,21 +214,3 @@
None
}
}
-
-/// Decodes the provided binary CBOR-encoded value and returns a
-/// ciborium::Value struct wrapped in Result.
-fn value_from_bytes(mut bytes: &[u8]) -> Result<Value> {
- let value = ciborium::de::from_reader(&mut bytes).map_err(BccError::CborDecodeError)?;
- // Ciborium tries to read one Value, but doesn't care if there is trailing data after it. We do.
- if !bytes.is_empty() {
- return Err(BccError::ExtraneousBytes);
- }
- Ok(value)
-}
-
-/// Encodes a ciborium::Value into bytes.
-fn value_to_bytes(value: &Value) -> Result<Vec<u8>> {
- let mut bytes: Vec<u8> = Vec::new();
- ciborium::ser::into_writer(&value, &mut bytes).map_err(BccError::CborEncodeError)?;
- Ok(bytes)
-}
diff --git a/pvmfw/src/dice.rs b/pvmfw/src/dice.rs
index 67865e5..da19931 100644
--- a/pvmfw/src/dice.rs
+++ b/pvmfw/src/dice.rs
@@ -13,16 +13,48 @@
// limitations under the License.
//! Support for DICE derivation and BCC generation.
+extern crate alloc;
+use alloc::format;
+use alloc::vec::Vec;
+use ciborium::cbor;
+use ciborium::Value;
use core::mem::size_of;
-use cstr::cstr;
use diced_open_dice::{
- bcc_format_config_descriptor, bcc_handover_main_flow, hash, Config, DiceConfigValues, DiceMode,
- Hash, InputValues, HIDDEN_SIZE,
+ bcc_handover_main_flow, hash, Config, DiceMode, Hash, InputValues, HIDDEN_SIZE,
};
use pvmfw_avb::{Capability, DebugLevel, Digest, VerifiedBootData};
use zerocopy::AsBytes;
+const COMPONENT_NAME_KEY: i64 = -70002;
+const SECURITY_VERSION_KEY: i64 = -70005;
+const RKP_VM_MARKER_KEY: i64 = -70006;
+// TODO(b/291245237): Document this key along with others used in ConfigDescriptor in AVF based VM.
+const INSTANCE_HASH_KEY: i64 = -71003;
+
+#[derive(Debug)]
+pub enum Error {
+ /// Error in CBOR operations
+ CborError(ciborium::value::Error),
+ /// Error in DICE operations
+ DiceError(diced_open_dice::DiceError),
+}
+
+impl From<ciborium::value::Error> for Error {
+ fn from(e: ciborium::value::Error) -> Self {
+ Self::CborError(e)
+ }
+}
+
+impl From<diced_open_dice::DiceError> for Error {
+ fn from(e: diced_open_dice::DiceError) -> Self {
+ Self::DiceError(e)
+ }
+}
+
+// DICE in pvmfw result type.
+type Result<T> = core::result::Result<T, Error>;
+
fn to_dice_mode(debug_level: DebugLevel) -> DiceMode {
match debug_level {
DebugLevel::None => DiceMode::kDiceModeNormal,
@@ -30,15 +62,16 @@
}
}
-fn to_dice_hash(verified_boot_data: &VerifiedBootData) -> diced_open_dice::Result<Hash> {
+fn to_dice_hash(verified_boot_data: &VerifiedBootData) -> Result<Hash> {
let mut digests = [0u8; size_of::<Digest>() * 2];
digests[..size_of::<Digest>()].copy_from_slice(&verified_boot_data.kernel_digest);
if let Some(initrd_digest) = verified_boot_data.initrd_digest {
digests[size_of::<Digest>()..].copy_from_slice(&initrd_digest);
}
- hash(&digests)
+ Ok(hash(&digests)?)
}
+#[derive(Clone)]
pub struct PartialInputs {
pub code_hash: Hash,
pub auth_hash: Hash,
@@ -48,7 +81,7 @@
}
impl PartialInputs {
- pub fn new(data: &VerifiedBootData) -> diced_open_dice::Result<Self> {
+ pub fn new(data: &VerifiedBootData) -> Result<Self> {
let code_hash = to_dice_hash(data)?;
let auth_hash = hash(data.public_key)?;
let mode = to_dice_mode(data.debug_level);
@@ -63,26 +96,36 @@
self,
current_bcc_handover: &[u8],
salt: &[u8; HIDDEN_SIZE],
+ instance_hash: Option<Hash>,
+ deferred_rollback_protection: bool,
next_bcc: &mut [u8],
- ) -> diced_open_dice::Result<()> {
- let mut config_descriptor_buffer = [0; 128];
- let config = self.generate_config_descriptor(&mut config_descriptor_buffer)?;
+ ) -> Result<()> {
+ let config = self
+ .generate_config_descriptor(instance_hash)
+ .map_err(|_| diced_open_dice::DiceError::InvalidInput)?;
let dice_inputs = InputValues::new(
self.code_hash,
- Config::Descriptor(config),
+ Config::Descriptor(&config),
self.auth_hash,
self.mode,
- self.make_hidden(salt)?,
+ self.make_hidden(salt, deferred_rollback_protection)?,
);
let _ = bcc_handover_main_flow(current_bcc_handover, &dice_inputs, next_bcc)?;
Ok(())
}
- fn make_hidden(&self, salt: &[u8; HIDDEN_SIZE]) -> diced_open_dice::Result<[u8; HIDDEN_SIZE]> {
+ fn make_hidden(
+ &self,
+ salt: &[u8; HIDDEN_SIZE],
+ deferred_rollback_protection: bool,
+ ) -> diced_open_dice::Result<[u8; HIDDEN_SIZE]> {
// We want to make sure we get a different sealing CDI for:
// - VMs with different salt values
// - An RKP VM and any other VM (regardless of salt)
+ // - depending on whether rollback protection has been deferred to payload. This ensures the
+ // adversary cannot leak the secrets by using old images & setting
+ // `deferred_rollback_protection` to true.
// The hidden input for DICE affects the sealing CDI (but the values in the config
// descriptor do not).
// Since the hidden input has to be a fixed size, create it as a hash of the values we
@@ -92,26 +135,34 @@
struct HiddenInput {
rkp_vm_marker: bool,
salt: [u8; HIDDEN_SIZE],
+ deferred_rollback_protection: bool,
}
- // TODO(b/291213394): Include `defer_rollback_protection` flag in the Hidden Input to
- // differentiate the secrets in both cases.
- hash(HiddenInput { rkp_vm_marker: self.rkp_vm_marker, salt: *salt }.as_bytes())
+ hash(
+ HiddenInput {
+ rkp_vm_marker: self.rkp_vm_marker,
+ salt: *salt,
+ deferred_rollback_protection,
+ }
+ .as_bytes(),
+ )
}
- fn generate_config_descriptor<'a>(
- &self,
- config_descriptor_buffer: &'a mut [u8],
- ) -> diced_open_dice::Result<&'a [u8]> {
- let config_values = DiceConfigValues {
- component_name: Some(cstr!("vm_entry")),
- security_version: if cfg!(dice_changes) { Some(self.security_version) } else { None },
- rkp_vm_marker: self.rkp_vm_marker,
- ..Default::default()
- };
- let config_descriptor_size =
- bcc_format_config_descriptor(&config_values, config_descriptor_buffer)?;
- let config = &config_descriptor_buffer[..config_descriptor_size];
- Ok(config)
+ fn generate_config_descriptor(&self, instance_hash: Option<Hash>) -> Result<Vec<u8>> {
+ let mut config = Vec::with_capacity(4);
+ config.push((cbor!(COMPONENT_NAME_KEY)?, cbor!("vm_entry")?));
+ if cfg!(dice_changes) {
+ config.push((cbor!(SECURITY_VERSION_KEY)?, cbor!(self.security_version)?));
+ }
+ if self.rkp_vm_marker {
+ config.push((cbor!(RKP_VM_MARKER_KEY)?, Value::Null))
+ }
+ if let Some(instance_hash) = instance_hash {
+ config.push((cbor!(INSTANCE_HASH_KEY)?, Value::from(instance_hash.as_slice())));
+ }
+ let config = Value::Map(config);
+ Ok(cbor_util::serialize(&config).map_err(|e| {
+ ciborium::value::Error::Custom(format!("Error in serialization: {e:?}"))
+ })?)
}
}
@@ -140,17 +191,24 @@
#[cfg(test)]
mod tests {
- use super::*;
+ use crate::{
+ Hash, PartialInputs, COMPONENT_NAME_KEY, INSTANCE_HASH_KEY, RKP_VM_MARKER_KEY,
+ SECURITY_VERSION_KEY,
+ };
use ciborium::Value;
+ use diced_open_dice::DiceArtifacts;
+ use diced_open_dice::DiceMode;
+ use diced_open_dice::HIDDEN_SIZE;
+ use pvmfw_avb::Capability;
+ use pvmfw_avb::DebugLevel;
+ use pvmfw_avb::Digest;
+ use pvmfw_avb::VerifiedBootData;
use std::collections::HashMap;
+ use std::mem::size_of;
use std::vec;
- const COMPONENT_NAME_KEY: i64 = -70002;
const COMPONENT_VERSION_KEY: i64 = -70003;
const RESETTABLE_KEY: i64 = -70004;
- const SECURITY_VERSION_KEY: i64 = -70005;
- const RKP_VM_MARKER_KEY: i64 = -70006;
-
const BASE_VB_DATA: VerifiedBootData = VerifiedBootData {
debug_level: DebugLevel::None,
kernel_digest: [1u8; size_of::<Digest>()],
@@ -159,6 +217,7 @@
capabilities: vec![],
rollback_index: 42,
};
+ const HASH: Hash = *b"sixtyfourbyteslongsentencearerarebutletsgiveitatrycantbethathard";
#[test]
fn base_data_conversion() {
@@ -193,7 +252,7 @@
fn base_config_descriptor() {
let vb_data = BASE_VB_DATA;
let inputs = PartialInputs::new(&vb_data).unwrap();
- let config_map = decode_config_descriptor(&inputs);
+ let config_map = decode_config_descriptor(&inputs, None);
assert_eq!(config_map.get(&COMPONENT_NAME_KEY).unwrap().as_text().unwrap(), "vm_entry");
assert_eq!(config_map.get(&COMPONENT_VERSION_KEY), None);
@@ -214,21 +273,104 @@
let vb_data =
VerifiedBootData { capabilities: vec![Capability::RemoteAttest], ..BASE_VB_DATA };
let inputs = PartialInputs::new(&vb_data).unwrap();
- let config_map = decode_config_descriptor(&inputs);
+ let config_map = decode_config_descriptor(&inputs, Some(HASH));
assert!(config_map.get(&RKP_VM_MARKER_KEY).unwrap().is_null());
}
- fn decode_config_descriptor(inputs: &PartialInputs) -> HashMap<i64, Value> {
- let mut buffer = [0; 128];
- let config_descriptor = inputs.generate_config_descriptor(&mut buffer).unwrap();
+ #[test]
+ fn config_descriptor_with_instance_hash() {
+ let vb_data =
+ VerifiedBootData { capabilities: vec![Capability::RemoteAttest], ..BASE_VB_DATA };
+ let inputs = PartialInputs::new(&vb_data).unwrap();
+ let config_map = decode_config_descriptor(&inputs, Some(HASH));
+ assert_eq!(*config_map.get(&INSTANCE_HASH_KEY).unwrap(), Value::from(HASH.as_slice()));
+ }
+
+ #[test]
+ fn config_descriptor_without_instance_hash() {
+ let vb_data =
+ VerifiedBootData { capabilities: vec![Capability::RemoteAttest], ..BASE_VB_DATA };
+ let inputs = PartialInputs::new(&vb_data).unwrap();
+ let config_map = decode_config_descriptor(&inputs, None);
+ assert!(!config_map.contains_key(&INSTANCE_HASH_KEY));
+ }
+
+ fn decode_config_descriptor(
+ inputs: &PartialInputs,
+ instance_hash: Option<Hash>,
+ ) -> HashMap<i64, Value> {
+ let config_descriptor = inputs.generate_config_descriptor(instance_hash).unwrap();
let cbor_map =
- cbor_util::deserialize::<Value>(config_descriptor).unwrap().into_map().unwrap();
+ cbor_util::deserialize::<Value>(&config_descriptor).unwrap().into_map().unwrap();
cbor_map
.into_iter()
.map(|(k, v)| ((k.into_integer().unwrap().try_into().unwrap()), v))
.collect()
}
+
+ #[test]
+ fn changing_deferred_rpb_changes_secrets() {
+ let vb_data = VerifiedBootData { debug_level: DebugLevel::Full, ..BASE_VB_DATA };
+ let inputs = PartialInputs::new(&vb_data).unwrap();
+ let mut buffer_without_defer = [0; 4096];
+ let mut buffer_with_defer = [0; 4096];
+ let mut buffer_without_defer_retry = [0; 4096];
+
+ let sample_dice_input: &[u8] = &[
+ 0xa3, // CDI attest
+ 0x01, 0x58, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // CDI seal
+ 0x02, 0x58, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // DICE chain
+ 0x03, 0x82, 0xa6, 0x01, 0x02, 0x03, 0x27, 0x04, 0x02, 0x20, 0x01, 0x21, 0x40, 0x22,
+ 0x40, 0x84, 0x40, 0xa0, 0x40, 0x40,
+ // 8-bytes of trailing data that aren't part of the DICE chain.
+ 0x84, 0x41, 0x55, 0xa0, 0x42, 0x11, 0x22, 0x40,
+ ];
+
+ inputs
+ .clone()
+ .write_next_bcc(
+ sample_dice_input,
+ &[0u8; HIDDEN_SIZE],
+ Some([0u8; 64]),
+ false,
+ &mut buffer_without_defer,
+ )
+ .unwrap();
+ let bcc_handover1 = diced_open_dice::bcc_handover_parse(&buffer_without_defer).unwrap();
+
+ inputs
+ .clone()
+ .write_next_bcc(
+ sample_dice_input,
+ &[0u8; HIDDEN_SIZE],
+ Some([0u8; 64]),
+ true,
+ &mut buffer_with_defer,
+ )
+ .unwrap();
+ let bcc_handover2 = diced_open_dice::bcc_handover_parse(&buffer_with_defer).unwrap();
+
+ inputs
+ .clone()
+ .write_next_bcc(
+ sample_dice_input,
+ &[0u8; HIDDEN_SIZE],
+ Some([0u8; 64]),
+ false,
+ &mut buffer_without_defer_retry,
+ )
+ .unwrap();
+ let bcc_handover3 =
+ diced_open_dice::bcc_handover_parse(&buffer_without_defer_retry).unwrap();
+
+ assert_ne!(bcc_handover1.cdi_seal(), bcc_handover2.cdi_seal());
+ assert_eq!(bcc_handover1.cdi_seal(), bcc_handover3.cdi_seal());
+ }
}
diff --git a/pvmfw/src/main.rs b/pvmfw/src/main.rs
index 2af19c4..299d1c0 100644
--- a/pvmfw/src/main.rs
+++ b/pvmfw/src/main.rs
@@ -143,10 +143,10 @@
RebootReason::InternalError
})?;
- let (new_instance, salt) = if cfg!(llpvm_changes)
- && should_defer_rollback_protection(fdt)?
- && verified_boot_data.has_capability(Capability::SecretkeeperProtection)
- {
+ let instance_hash = if cfg!(llpvm_changes) { Some(salt_from_instance_id(fdt)?) } else { None };
+ let defer_rollback_protection = should_defer_rollback_protection(fdt)?
+ && verified_boot_data.has_capability(Capability::SecretkeeperProtection);
+ let (new_instance, salt) = if defer_rollback_protection {
info!("Guest OS is capable of Secretkeeper protection, deferring rollback protection");
// rollback_index of the image is used as security_version and is expected to be > 0 to
// discourage implicit allocation.
@@ -155,7 +155,7 @@
return Err(RebootReason::InvalidPayload);
};
// `new_instance` cannot be known to pvmfw
- (false, salt_from_instance_id(fdt)?)
+ (false, instance_hash.unwrap())
} else {
let (recorded_entry, mut instance_img, header_index) =
get_recorded_entry(&mut pci_root, cdi_seal).map_err(|e| {
@@ -164,18 +164,15 @@
})?;
let (new_instance, salt) = if let Some(entry) = recorded_entry {
maybe_check_dice_measurements_match_entry(&dice_inputs, &entry)?;
- let salt = if cfg!(llpvm_changes) { salt_from_instance_id(fdt)? } else { entry.salt };
+ let salt = instance_hash.unwrap_or(entry.salt);
(false, salt)
} else {
// New instance!
- let salt = if cfg!(llpvm_changes) {
- salt_from_instance_id(fdt)?
- } else {
- rand::random_array().map_err(|e| {
- error!("Failed to generated instance.img salt: {e}");
- RebootReason::InternalError
- })?
- };
+ let salt = instance_hash.map_or_else(rand::random_array, Ok).map_err(|e| {
+ error!("Failed to generated instance.img salt: {e}");
+ RebootReason::InternalError
+ })?;
+
let entry = EntryBody::new(&dice_inputs, &salt);
record_instance_entry(&entry, cdi_seal, &mut instance_img, header_index).map_err(
|e| {
@@ -204,10 +201,18 @@
Cow::Owned(truncated_bcc_handover)
};
- dice_inputs.write_next_bcc(new_bcc_handover.as_ref(), &salt, next_bcc).map_err(|e| {
- error!("Failed to derive next-stage DICE secrets: {e:?}");
- RebootReason::SecretDerivationError
- })?;
+ dice_inputs
+ .write_next_bcc(
+ new_bcc_handover.as_ref(),
+ &salt,
+ instance_hash,
+ defer_rollback_protection,
+ next_bcc,
+ )
+ .map_err(|e| {
+ error!("Failed to derive next-stage DICE secrets: {e:?}");
+ RebootReason::SecretDerivationError
+ })?;
flush(next_bcc);
let kaslr_seed = u64::from_ne_bytes(rand::random_array().map_err(|e| {
diff --git a/service_vm/requests/src/rkp.rs b/service_vm/requests/src/rkp.rs
index aa363e5..c62a36b 100644
--- a/service_vm/requests/src/rkp.rs
+++ b/service_vm/requests/src/rkp.rs
@@ -127,7 +127,7 @@
"product" => "avf",
"vb_state" => "avf",
"manufacturer" => "aosp-avf",
- "vbmeta_digest" => Value::Bytes(vec![1u8; 0]),
+ "vbmeta_digest" => Value::Bytes(vec![1u8; 1]),
"security_level" => "avf",
"boot_patch_level" => 20240202,
"bootloader_state" => "avf",
diff --git a/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java b/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java
index b6003f6..46df011 100644
--- a/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java
+++ b/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java
@@ -55,6 +55,7 @@
protected static final String LOG_PATH = TEST_ROOT + "log.txt";
protected static final String CONSOLE_PATH = TEST_ROOT + "console.txt";
protected static final String TRADEFED_CONSOLE_PATH = TRADEFED_TEST_ROOT + "console.txt";
+ protected static final String TRADEFED_LOG_PATH = TRADEFED_TEST_ROOT + "log.txt";
private static final int TEST_VM_ADB_PORT = 8000;
private static final String MICRODROID_SERIAL = "localhost:" + TEST_VM_ADB_PORT;
private static final String INSTANCE_IMG = "instance.img";
diff --git a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
index e676841..9d0b04b 100644
--- a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
+++ b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
@@ -877,10 +877,13 @@
assertWithMessage("Incorrect ABI list").that(abis).hasLength(1);
// Check that no denials have happened so far
- String logText =
- getDevice().pullFileContents(CONSOLE_PATH) + getDevice().pullFileContents(LOG_PATH);
+ String consoleText = getDevice().pullFileContents(TRADEFED_CONSOLE_PATH);
+ assertWithMessage("Console output shouldn't be empty").that(consoleText).isNotEmpty();
+ String logText = getDevice().pullFileContents(TRADEFED_LOG_PATH);
+ assertWithMessage("Log output shouldn't be empty").that(logText).isNotEmpty();
+
assertWithMessage("Unexpected denials during VM boot")
- .that(logText)
+ .that(consoleText + logText)
.doesNotContainMatch("avc:\\s+denied");
assertThat(getDeviceNumCpus(microdroid)).isEqualTo(getDeviceNumCpus(android));
@@ -1177,6 +1180,40 @@
}
}
+ @Test
+ public void testHugePages() throws Exception {
+ ITestDevice device = getDevice();
+ boolean disableRoot = !device.isAdbRoot();
+ CommandRunner android = new CommandRunner(device);
+
+ final String SHMEM_ENABLED_PATH = "/sys/kernel/mm/transparent_hugepage/shmem_enabled";
+ String thpShmemStr = android.run("cat", SHMEM_ENABLED_PATH);
+
+ assumeFalse("shmem already enabled, skip", thpShmemStr.contains("[advise]"));
+ assumeTrue("Unsupported shmem, skip", thpShmemStr.contains("[never]"));
+
+ device.enableAdbRoot();
+ assumeTrue("adb root is not enabled", device.isAdbRoot());
+ android.run("echo advise > " + SHMEM_ENABLED_PATH);
+
+ final String configPath = "assets/vm_config.json";
+ mMicrodroidDevice =
+ MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
+ .debugLevel("full")
+ .memoryMib(minMemorySize())
+ .cpuTopology("match_host")
+ .protectedVm(mProtectedVm)
+ .gki(mGki)
+ .hugePages(true)
+ .build(getAndroidDevice());
+ mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
+
+ android.run("echo never >" + SHMEM_ENABLED_PATH);
+ if (disableRoot) {
+ device.disableAdbRoot();
+ }
+ }
+
@Before
public void setUp() throws Exception {
assumeDeviceIsCapable(getDevice());
diff --git a/tests/pvmfw/helper/Android.bp b/tests/pvmfw/helper/Android.bp
index 90ca03e..a75f034 100644
--- a/tests/pvmfw/helper/Android.bp
+++ b/tests/pvmfw/helper/Android.bp
@@ -5,7 +5,7 @@
java_library_host {
name: "PvmfwHostTestHelper",
srcs: ["java/**/*.java"],
- libs: [
+ static_libs: [
"androidx.annotation_annotation",
"truth",
],
diff --git a/tests/pvmfw/tools/PvmfwTool.java b/tests/pvmfw/tools/PvmfwTool.java
index e150ec4..9f0cb42 100644
--- a/tests/pvmfw/tools/PvmfwTool.java
+++ b/tests/pvmfw/tools/PvmfwTool.java
@@ -25,10 +25,10 @@
public class PvmfwTool {
public static void printUsage() {
System.out.println("pvmfw-tool: Appends pvmfw.bin and config payloads.");
- System.out.println(" Requires BCC and VM reference DT.");
- System.out.println(" VM DTBO and Debug policy can optionally be specified");
+ System.out.println(" Requires BCC. VM Reference DT, VM DTBO, and Debug policy");
+ System.out.println(" can optionally be specified");
System.out.println(
- "Usage: pvmfw-tool <out> <pvmfw.bin> <bcc.dat> <VM reference DT> [VM DTBO] [debug"
+ "Usage: pvmfw-tool <out> <pvmfw.bin> <bcc.dat> [VM reference DT] [VM DTBO] [debug"
+ " policy]");
}
@@ -41,10 +41,13 @@
File out = new File(args[0]);
File pvmfwBin = new File(args[1]);
File bccData = new File(args[2]);
- File vmReferenceDt = new File(args[3]);
+ File vmReferenceDt = null;
File vmDtbo = null;
File dp = null;
+ if (args.length > 3) {
+ vmReferenceDt = new File(args[3]);
+ }
if (args.length > 4) {
vmDtbo = new File(args[4]);
}
@@ -53,12 +56,18 @@
}
try {
- Pvmfw pvmfw =
+ Pvmfw.Builder builder =
new Pvmfw.Builder(pvmfwBin, bccData)
.setVmReferenceDt(vmReferenceDt)
.setDebugPolicyOverlay(dp)
- .setVmDtbo(vmDtbo)
- .build();
+ .setVmDtbo(vmDtbo);
+ if (vmReferenceDt == null) {
+ builder.setVersion(1, 1);
+ } else {
+ builder.setVersion(1, 2);
+ }
+
+ Pvmfw pvmfw = builder.build();
pvmfw.serialize(out);
} catch (IOException e) {
e.printStackTrace();
diff --git a/virtualizationmanager/Android.bp b/virtualizationmanager/Android.bp
index d8f8209..d1ef4de 100644
--- a/virtualizationmanager/Android.bp
+++ b/virtualizationmanager/Android.bp
@@ -100,3 +100,21 @@
],
test_suites: ["general-tests"],
}
+
+cc_fuzz {
+ name: "virtualizationmanager_fuzzer",
+ defaults: ["service_fuzzer_defaults"],
+ srcs: ["fuzzer.cpp"],
+ static_libs: [
+ "android.system.virtualizationservice-ndk",
+ "libbase",
+ ],
+ shared_libs: [
+ "libbinder_ndk",
+ "libbinder_rpc_unstable",
+ "liblog",
+ ],
+ fuzz_config: {
+ cc: ["android-kvm@google.com"],
+ },
+}
diff --git a/virtualizationmanager/fuzzer.cpp b/virtualizationmanager/fuzzer.cpp
new file mode 100644
index 0000000..6afea46
--- /dev/null
+++ b/virtualizationmanager/fuzzer.cpp
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include <aidl/android/system/virtualizationservice/IVirtualizationService.h>
+#include <android-base/file.h>
+#include <android-base/result.h>
+#include <android-base/unique_fd.h>
+#include <fuzzbinder/libbinder_ndk_driver.h>
+#include <fuzzer/FuzzedDataProvider.h>
+#include <unistd.h>
+
+#include <binder_rpc_unstable.hpp>
+#include <cstdlib>
+#include <iostream>
+
+using aidl::android::system::virtualizationservice::IVirtualizationService;
+using android::fuzzService;
+using android::base::ErrnoError;
+using android::base::Error;
+using android::base::Pipe;
+using android::base::Result;
+using android::base::Socketpair;
+using android::base::unique_fd;
+using ndk::SpAIBinder;
+
+static constexpr const char VIRTMGR_PATH[] = "/apex/com.android.virt/bin/virtmgr";
+static constexpr size_t VIRTMGR_THREADS = 2;
+
+Result<unique_fd> get_service_fd() {
+ unique_fd server_fd, client_fd;
+ if (!Socketpair(SOCK_STREAM, &server_fd, &client_fd)) {
+ return ErrnoError() << "Failed to create socketpair";
+ }
+
+ unique_fd wait_fd, ready_fd;
+ if (!Pipe(&wait_fd, &ready_fd, 0)) {
+ return ErrnoError() << "Failed to create pipe";
+ }
+
+ if (int pid = fork(); pid == 0) {
+ client_fd.reset();
+ wait_fd.reset();
+
+ auto server_fd_str = std::to_string(server_fd.get());
+ auto ready_fd_str = std::to_string(ready_fd.get());
+
+ if (execl(VIRTMGR_PATH, VIRTMGR_PATH, "--rpc-server-fd", server_fd_str.c_str(),
+ "--ready-fd", ready_fd_str.c_str(), nullptr) == -1) {
+ return ErrnoError() << "Failed to execute virtmgr";
+ }
+ } else if (pid < 0) {
+ return ErrnoError() << "Failed to fork";
+ }
+
+ server_fd.reset();
+ ready_fd.reset();
+
+ char buf;
+ if (read(wait_fd.get(), &buf, sizeof(buf)) < 0) {
+ return ErrnoError() << "Failed to wait for VirtualizationService to be ready";
+ }
+
+ return client_fd;
+}
+
+Result<std::shared_ptr<IVirtualizationService>> connect_service(int fd) {
+ std::unique_ptr<ARpcSession, decltype(&ARpcSession_free)> session(ARpcSession_new(),
+ &ARpcSession_free);
+ ARpcSession_setFileDescriptorTransportMode(session.get(),
+ ARpcSession_FileDescriptorTransportMode::Unix);
+ ARpcSession_setMaxIncomingThreads(session.get(), VIRTMGR_THREADS);
+ ARpcSession_setMaxOutgoingConnections(session.get(), VIRTMGR_THREADS);
+ AIBinder* binder = ARpcSession_setupUnixDomainBootstrapClient(session.get(), fd);
+ if (binder == nullptr) {
+ return Error() << "Failed to connect to VirtualizationService";
+ }
+ return IVirtualizationService::fromBinder(SpAIBinder{binder});
+}
+
+Result<void> inner_fuzz(const uint8_t* data, size_t size) {
+ unique_fd fd = OR_RETURN(get_service_fd());
+ std::shared_ptr<IVirtualizationService> service = OR_RETURN(connect_service(fd.get()));
+ fuzzService(service->asBinder().get(), FuzzedDataProvider(data, size));
+
+ return {};
+}
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+ if (auto ret = inner_fuzz(data, size); !ret.ok()) {
+ std::cerr << "connecting to service failed: " << ret.error() << std::endl;
+ abort();
+ }
+ return 0;
+}
diff --git a/virtualizationmanager/src/aidl.rs b/virtualizationmanager/src/aidl.rs
index aeee6f7..dd17b46 100644
--- a/virtualizationmanager/src/aidl.rs
+++ b/virtualizationmanager/src/aidl.rs
@@ -607,16 +607,25 @@
};
// Create TAP network interface if the VM supports network.
- let _tap_fd = if cfg!(network) && config.networkSupported {
+ let tap = if cfg!(network) && config.networkSupported {
if *is_protected {
return Err(anyhow!("Network feature is not supported for pVM yet"))
.with_log()
.or_binder_exception(ExceptionCode::UNSUPPORTED_OPERATION)?;
}
- Some(GLOBAL_SERVICE.createTapInterface(&get_this_pid().to_string())?)
+ Some(File::from(
+ GLOBAL_SERVICE
+ .createTapInterface(&get_this_pid().to_string())?
+ .as_ref()
+ .try_clone()
+ .context("Failed to get TAP interface from ParcelFileDescriptor")
+ .or_binder_exception(ExceptionCode::BAD_PARCELABLE)?,
+ ))
} else {
None
};
+ let virtio_snd_backend =
+ if cfg!(paravirtualized_devices) { Some(String::from("aaudio")) } else { None };
// Actually start the VM.
let crosvm_config = CrosvmConfig {
@@ -646,6 +655,8 @@
display_config,
input_device_options,
hugepages: config.hugePages,
+ tap,
+ virtio_snd_backend,
};
let instance = Arc::new(
VmInstance::new(
diff --git a/virtualizationmanager/src/crosvm.rs b/virtualizationmanager/src/crosvm.rs
index d48ef7b..371a908 100644
--- a/virtualizationmanager/src/crosvm.rs
+++ b/virtualizationmanager/src/crosvm.rs
@@ -122,6 +122,8 @@
pub display_config: Option<DisplayConfig>,
pub input_device_options: Vec<InputDeviceOption>,
pub hugepages: bool,
+ pub tap: Option<File>,
+ pub virtio_snd_backend: Option<String>,
}
#[derive(Debug)]
@@ -979,7 +981,7 @@
}
if cfg!(paravirtualized_devices) {
- // TODO(b/325929096): Need to set up network from the config
+ // TODO(b/340376951): Remove this after tap in CrosvmConfig is connected to tethering.
if rustutils::system_properties::read_bool("ro.crosvm.network.setup.done", false)
.unwrap_or(false)
{
@@ -987,6 +989,14 @@
}
}
+ if cfg!(network) {
+ if let Some(tap) = &config.tap {
+ let tap_fd = tap.as_raw_fd();
+ preserved_fds.push(tap_fd);
+ command.arg("--net").arg(format!("tap-fd={}", tap_fd));
+ }
+ }
+
if cfg!(paravirtualized_devices) {
for input_device_option in config.input_device_options.iter() {
command.arg("--input");
@@ -1020,6 +1030,12 @@
debug!("Preserving FDs {:?}", preserved_fds);
command.preserved_fds(preserved_fds);
+ if cfg!(paravirtualized_devices) {
+ if let Some(virtio_snd_backend) = &config.virtio_snd_backend {
+ command.arg("--virtio-snd").arg(format!("backend={}", virtio_snd_backend));
+ }
+ }
+
print_crosvm_args(&command);
let result = SharedChild::spawn(&mut command)?;
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index 5e71245..8fe4167 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -346,7 +346,7 @@
))
.with_log();
}
- if !remotely_provisioned_component_service_exists()? {
+ if !is_remote_provisioning_hal_declared()? {
return Err(Status::new_exception_str(
ExceptionCode::UNSUPPORTED_OPERATION,
Some("AVF remotely provisioned component service is not declared"),
@@ -403,7 +403,7 @@
}
fn isRemoteAttestationSupported(&self) -> binder::Result<bool> {
- remotely_provisioned_component_service_exists()
+ is_remote_provisioning_hal_declared()
}
fn getAssignableDevices(&self) -> binder::Result<Vec<AssignableDevice>> {
@@ -862,7 +862,9 @@
Ok(())
}
-fn remotely_provisioned_component_service_exists() -> binder::Result<bool> {
+/// Returns true if the AVF remotely provisioned component service is declared in the
+/// VINTF manifest.
+pub(crate) fn is_remote_provisioning_hal_declared() -> binder::Result<bool> {
Ok(binder::is_declared(REMOTELY_PROVISIONED_COMPONENT_SERVICE_NAME)?)
}
diff --git a/virtualizationservice/src/main.rs b/virtualizationservice/src/main.rs
index 8acfdd3..55245f6 100644
--- a/virtualizationservice/src/main.rs
+++ b/virtualizationservice/src/main.rs
@@ -20,7 +20,10 @@
mod remote_provisioning;
mod rkpvm;
-use crate::aidl::{remove_temporary_dir, VirtualizationServiceInternal, TEMPORARY_DIRECTORY};
+use crate::aidl::{
+ is_remote_provisioning_hal_declared, remove_temporary_dir, VirtualizationServiceInternal,
+ TEMPORARY_DIRECTORY,
+};
use android_logger::{Config, FilterBuilder};
use android_system_virtualizationmaintenance::aidl::android::system::virtualizationmaintenance;
use android_system_virtualizationservice_internal::aidl::android::system::virtualizationservice_internal;
@@ -81,7 +84,7 @@
BnVirtualizationServiceInternal::new_binder(service.clone(), BinderFeatures::default());
register(INTERNAL_SERVICE_NAME, internal_service)?;
- if cfg!(remote_attestation) {
+ if is_remote_provisioning_hal_declared().unwrap_or(false) {
// The IRemotelyProvisionedComponent service is only supposed to be triggered by rkpd for
// RKP VM attestation.
let remote_provisioning_service = remote_provisioning::new_binder();
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..a206c25 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, IFF_VNET_HDR, 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 | IFF_VNET_HDR) 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..4d79235 100644
--- a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
+++ b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
@@ -19,40 +19,49 @@
import static android.system.virtualmachine.VirtualMachineConfig.CPU_TOPOLOGY_MATCH_HOST;
import android.app.Activity;
+import android.crosvm.ICrosvmAndroidDisplayService;
+import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.os.Bundle;
+import android.os.ParcelFileDescriptor;
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 libcore.io.IoBridge;
+
+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.ByteBuffer;
+import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
@@ -63,8 +72,9 @@
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 ParcelFileDescriptor mCursorStream;
private VirtualMachineConfig createVirtualMachineConfig(String jsonPath) {
VirtualMachineConfig.Builder configBuilder =
@@ -75,6 +85,7 @@
if (DEBUG) {
configBuilder.setDebugLevel(VirtualMachineConfig.DEBUG_LEVEL_FULL);
configBuilder.setVmOutputCaptured(true);
+ configBuilder.setConnectVmConsole(true);
}
VirtualMachineCustomImageConfig.Builder customImageConfigBuilder =
new VirtualMachineCustomImageConfig.Builder();
@@ -160,6 +171,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,14 +251,19 @@
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);
}
SurfaceView surfaceView = findViewById(R.id.surface_view);
+ SurfaceView cursorSurfaceView = findViewById(R.id.cursor_surface_view);
+ cursorSurfaceView.setZOrderMediaOverlay(true);
View backgroundTouchView = findViewById(R.id.background_touch_view);
backgroundTouchView.setOnTouchListener(
(v, event) -> {
@@ -280,7 +297,10 @@
+ holder.getSurface()
+ ")");
runWithDisplayService(
- (service) -> service.setSurface(holder.getSurface()));
+ (service) ->
+ service.setSurface(
+ holder.getSurface(),
+ false /* forCursor */));
}
@Override
@@ -292,7 +312,52 @@
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
Log.d(TAG, "ICrosvmAndroidDisplayService.removeSurface()");
- runWithDisplayService((service) -> service.removeSurface());
+ runWithDisplayService(
+ (service) -> service.removeSurface(false /* forCursor */));
+ }
+ });
+ cursorSurfaceView.getHolder().setFormat(PixelFormat.RGBA_8888);
+ cursorSurfaceView
+ .getHolder()
+ .addCallback(
+ new SurfaceHolder.Callback() {
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ try {
+ ParcelFileDescriptor[] pfds =
+ ParcelFileDescriptor.createSocketPair();
+ mExecutorService.execute(
+ new CursorHandler(cursorSurfaceView, pfds[0]));
+ mCursorStream = pfds[0];
+ runWithDisplayService(
+ (service) -> service.setCursorStream(pfds[1]));
+ } catch (Exception e) {
+ Log.d("TAG", "failed to run cursor stream handler", e);
+ }
+ runWithDisplayService(
+ (service) ->
+ service.setSurface(
+ holder.getSurface(), true /* forCursor */));
+ }
+
+ @Override
+ public void surfaceChanged(
+ SurfaceHolder holder, int format, int width, int height) {
+ Log.d(TAG, "width: " + width + ", height: " + height);
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ Log.d(TAG, "ICrosvmAndroidDisplayService.removeSurface()");
+ runWithDisplayService(
+ (service) -> service.removeSurface(true /* forCursor */));
+ if (mCursorStream != null) {
+ try {
+ mCursorStream.close();
+ } catch (IOException e) {
+ Log.d(TAG, "failed to close fd", e);
+ }
+ }
}
});
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
@@ -305,6 +370,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) {
@@ -336,6 +410,43 @@
}
}
+ static class CursorHandler implements Runnable {
+ private final SurfaceView mSurfaceView;
+ private final ParcelFileDescriptor mStream;
+
+ CursorHandler(SurfaceView s, ParcelFileDescriptor stream) {
+ mSurfaceView = s;
+ mStream = stream;
+ }
+
+ @Override
+ public void run() {
+ Log.d(TAG, "CursorHandler");
+ try {
+ ByteBuffer byteBuffer = ByteBuffer.allocate(8 /* (x: u32, y: u32) */);
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+ while (true) {
+ byteBuffer.clear();
+ int bytes =
+ IoBridge.read(
+ mStream.getFileDescriptor(),
+ byteBuffer.array(),
+ 0,
+ byteBuffer.array().length);
+ float x = (float) (byteBuffer.getInt() & 0xFFFFFFFF);
+ float y = (float) (byteBuffer.getInt() & 0xFFFFFFFF);
+ mSurfaceView.post(
+ () -> {
+ mSurfaceView.setTranslationX(x);
+ mSurfaceView.setTranslationY(y);
+ });
+ }
+ } catch (IOException e) {
+ Log.e(TAG, e.getMessage());
+ }
+ }
+ }
+
/** Reads data from an input stream and posts it to the output data */
static class Reader implements Runnable {
private final String mName;
@@ -359,4 +470,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;
+ }
+ }
+ }
+ }
}
diff --git a/vmlauncher_app/res/layout/activity_main.xml b/vmlauncher_app/res/layout/activity_main.xml
index e52dfcd..a80ece0 100644
--- a/vmlauncher_app/res/layout/activity_main.xml
+++ b/vmlauncher_app/res/layout/activity_main.xml
@@ -11,7 +11,7 @@
android:layout_height="match_parent"
/>
<SurfaceView
- android:id="@+id/surface_view"
+ android:id="@+id/surface_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusable="true"
@@ -20,5 +20,11 @@
android:defaultFocusHighlightEnabled="true">
<requestFocus />
</SurfaceView>
+ <!-- A cursor size in virtio-gpu spec is always 64x64 -->
+ <SurfaceView
+ android:id="@+id/cursor_surface_view"
+ android:layout_width="64px"
+ android:layout_height="64px">
+ </SurfaceView>
</merge>