Add basic test that shares a VM from one app to another
This test consist of 2 apps:
* MicrodroidTestApp - test driver
* MicrodroidVmShareApp - helper app that exposes a service that
MicrodroidTestApp binds to.
The test orchestarted by MicrodroidTestApp consists of the following:
1. MicrodroidTestApp starts & stops a VM
2. MicrodroidTestApp creates a descriptor of that VM
3. MicrodroidTestApp binds to service in MicrodroidVmShareApp
4. MicrodroidTestApp sends the descriptor to MicrodroidVmShareApp
5. MicrodroidVmShareApp starts a VM from that descriptor, and connects
to the vsock service exposed by the VM.
6. MicrodroidVmShareApp shares a binder interface for MicrodroidTestApp
to interact with the service exposed by the MicrodroidVmShareApp VM.
7. MicrodroidTestApp uses that binder to assert on the VM.
This change adds the scaffolding and a basic test, more involved test
(e.g. with trusted storage) will be added in the follow up changes.
Bug: 259384440
Test: atest MicrodroidTestApp
Change-Id: I924c0fd9494010fd55fd9062206e8f3202e43b5b
diff --git a/tests/testapk/Android.bp b/tests/testapk/Android.bp
index 3c487ee..bafab53 100644
--- a/tests/testapk/Android.bp
+++ b/tests/testapk/Android.bp
@@ -8,6 +8,10 @@
"cts",
"general-tests",
],
+ static_libs: [
+ "com.android.microdroid.testservice-java",
+ "com.android.microdroid.test.vmshare_service-java",
+ ],
sdk_version: "test_current",
jni_uses_platform_apis: true,
use_embedded_native_libs: true,
@@ -25,7 +29,6 @@
"androidx.test.ext.junit",
"authfs_test_apk_assets",
"cbor-java",
- "com.android.microdroid.testservice-java",
"truth-prebuilt",
"compatibility-common-util-devicesidelib",
],
diff --git a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
index 926dd77..13d56e1 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -24,15 +24,20 @@
import static android.system.virtualmachine.VirtualMachineManager.CAPABILITY_PROTECTED_VM;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.common.truth.TruthJUnit.assume;
import static org.junit.Assert.assertThrows;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
+import android.content.ComponentName;
import android.content.Context;
import android.content.ContextWrapper;
+import android.content.Intent;
+import android.content.ServiceConnection;
import android.os.Build;
+import android.os.IBinder;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.os.ParcelFileDescriptor.AutoCloseInputStream;
@@ -50,6 +55,7 @@
import com.android.compatibility.common.util.CddTest;
import com.android.microdroid.test.device.MicrodroidDeviceTestBase;
+import com.android.microdroid.test.vmshare.IVmShareTestService;
import com.android.microdroid.testservice.ITestService;
import com.google.common.base.Strings;
@@ -86,6 +92,8 @@
import java.util.OptionalLong;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import co.nstant.in.cbor.CborDecoder;
@@ -1633,6 +1641,76 @@
assertThat(testResults.mFileContent).isEqualTo("not a secret!");
}
+ @Test
+ public void testShareVmWithAnotherApp() throws Exception {
+ assumeSupportedKernel();
+
+ Context ctx = getContext();
+ Context otherAppCtx = ctx.createPackageContext(VM_SHARE_APP_PACKAGE_NAME, 0);
+
+ VirtualMachineConfig config =
+ new VirtualMachineConfig.Builder(otherAppCtx)
+ .setDebugLevel(DEBUG_LEVEL_FULL)
+ .setProtectedVm(isProtectedVm())
+ .setPayloadBinaryName("MicrodroidPayloadInOtherAppNativeLib.so")
+ .build();
+
+ VirtualMachine vm = forceCreateNewVirtualMachine("vm_to_share", config);
+ // Just start & stop the VM.
+ runVmTestService(vm, (ts, tr) -> {});
+ // Get a descriptor that we will share with another app (VM_SHARE_APP_PACKAGE_NAME)
+ VirtualMachineDescriptor vmDesc = vm.toDescriptor();
+
+ Intent serviceIntent = new Intent();
+ serviceIntent.setComponent(
+ new ComponentName(
+ VM_SHARE_APP_PACKAGE_NAME,
+ "com.android.microdroid.test.sharevm.VmShareServiceImpl"));
+
+ VmShareServiceConnection connection = new VmShareServiceConnection();
+ boolean ret = ctx.bindService(serviceIntent, connection, Context.BIND_AUTO_CREATE);
+ assertWithMessage("Failed to bind to " + serviceIntent).that(ret).isTrue();
+
+ IVmShareTestService service = connection.waitForService();
+ assertWithMessage("Timed out connecting to " + serviceIntent).that(service).isNotNull();
+
+ try {
+ // Send the VM descriptor to the other app. When received, it will reconstruct the VM
+ // from the descriptor, start it, connect to the ITestService in it, creates a "proxy"
+ // ITestService binder that delegates all the calls to the VM, and share it with this
+ // app. It will allow us to verify assertions on the running VM in the other app.
+ ITestService testServiceProxy = service.startVm(vmDesc);
+
+ int result = testServiceProxy.addInteger(37, 73);
+ assertThat(result).isEqualTo(110);
+ } finally {
+ ctx.unbindService(connection);
+ }
+ }
+
+ private static class VmShareServiceConnection implements ServiceConnection {
+
+ private final CountDownLatch mLatch = new CountDownLatch(1);
+
+ private IVmShareTestService mVmShareTestService;
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ mVmShareTestService = IVmShareTestService.Stub.asInterface(service);
+ mLatch.countDown();
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {}
+
+ private IVmShareTestService waitForService() throws Exception {
+ if (!mLatch.await(1, TimeUnit.MINUTES)) {
+ return null;
+ }
+ return mVmShareTestService;
+ }
+ }
+
private VirtualMachineDescriptor toParcelFromParcel(VirtualMachineDescriptor descriptor) {
Parcel parcel = Parcel.obtain();
descriptor.writeToParcel(parcel, 0);
diff --git a/tests/vmshareapp/Android.bp b/tests/vmshareapp/Android.bp
index 2b117a1..6c2c9e4 100644
--- a/tests/vmshareapp/Android.bp
+++ b/tests/vmshareapp/Android.bp
@@ -5,6 +5,7 @@
// Helper app to verify that we can create a VM using others app payload, and share VMs between apps
android_test_helper_app {
name: "MicrodroidVmShareApp",
+ srcs: ["src/java/**/*.java"],
// Defaults are defined in ../testapk/Android.bp
defaults: ["MicrodroidTestAppsDefaults"],
jni_libs: [
diff --git a/tests/vmshareapp/AndroidManifest.xml b/tests/vmshareapp/AndroidManifest.xml
index eed3364..b623f7f 100644
--- a/tests/vmshareapp/AndroidManifest.xml
+++ b/tests/vmshareapp/AndroidManifest.xml
@@ -20,5 +20,13 @@
<uses-feature android:name="android.software.virtualization_framework"
android:required="false" />
- <application />
+ <application>
+ <service android:name="com.android.microdroid.test.sharevm.VmShareServiceImpl"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="com.android.microdroid.test.sharevm.VmShareService"/>
+ </intent-filter>
+ </service>
+ </application>
+
</manifest>
diff --git a/tests/vmshareapp/aidl/Android.bp b/tests/vmshareapp/aidl/Android.bp
new file mode 100644
index 0000000..df4a4b4
--- /dev/null
+++ b/tests/vmshareapp/aidl/Android.bp
@@ -0,0 +1,15 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+// Unfortunatelly aidl_interface doesn't work well with .aidl files that depend on java-only
+// parcelables (e.g. Bundle, VirtualMachineDescriptor), hence this java_library.
+java_library {
+ name: "com.android.microdroid.test.vmshare_service-java",
+ srcs: ["com/**/*.aidl"],
+ sdk_version: "test_current",
+ static_libs: ["com.android.microdroid.testservice-java"],
+ aidl: {
+ include_dirs: ["packages/modules/Virtualization/tests/aidl/"],
+ },
+}
diff --git a/tests/vmshareapp/aidl/com/android/microdroid/test/vmshare/IVmShareTestService.aidl b/tests/vmshareapp/aidl/com/android/microdroid/test/vmshare/IVmShareTestService.aidl
new file mode 100644
index 0000000..fe6ca43
--- /dev/null
+++ b/tests/vmshareapp/aidl/com/android/microdroid/test/vmshare/IVmShareTestService.aidl
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2023 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.
+ */
+package com.android.microdroid.test.vmshare;
+
+import android.system.virtualmachine.VirtualMachineDescriptor;
+import com.android.microdroid.testservice.ITestService;
+
+/** {@hide} */
+interface IVmShareTestService {
+ ITestService startVm(in VirtualMachineDescriptor vmDesc);
+}
diff --git a/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java b/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java
new file mode 100644
index 0000000..215bc6d
--- /dev/null
+++ b/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+package com.android.microdroid.test.sharevm;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.system.virtualmachine.VirtualMachine;
+import android.system.virtualmachine.VirtualMachineCallback;
+import android.system.virtualmachine.VirtualMachineDescriptor;
+import android.system.virtualmachine.VirtualMachineException;
+import android.system.virtualmachine.VirtualMachineManager;
+import android.util.Log;
+
+import com.android.microdroid.test.vmshare.IVmShareTestService;
+import com.android.microdroid.testservice.ITestService;
+
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * A {@link Service} that is used in end-to-end tests of the {@link VirtualMachine} sharing
+ * functionality.
+ *
+ * <p>During the test {@link com.android.microdroid.test.MicrodroidTests} will bind to this service,
+ * and call {@link #startVm(VirtualMachineDescriptor)} to share the VM. This service then will
+ * create a {@link VirtualMachine} from that descriptor, {@link VirtualMachine#run() run} it, and
+ * send back {@link RemoteTestServiceDelegate}. The {@code MicrodroidTests} can use that {@link
+ * RemoteTestServiceDelegate} to assert conditions on the VM running in the {@link
+ * VmShareServiceImpl}.
+ *
+ * <p>The {@link VirtualMachine} running in this service will be stopped on {@link
+ * #onUnbind(Intent)}.
+ *
+ * @see com.android.microdroid.test.MicrodroidTests#testShareVmWithAnotherApp
+ */
+public class VmShareServiceImpl extends Service {
+
+ private static final String TAG = "VmShareApp";
+
+ private IVmShareTestService.Stub mBinder;
+
+ private VirtualMachine mVirtualMachine;
+
+ @Override
+ public void onCreate() {
+ mBinder = new ServiceImpl();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ Log.i(TAG, "onBind " + intent + " binder = " + mBinder);
+ return mBinder;
+ }
+
+ @Override
+ public boolean onUnbind(Intent intent) {
+ deleteVm();
+ // Tell framework that it shouldn't call onRebind.
+ return false;
+ }
+
+ private void deleteVm() {
+ if (mVirtualMachine == null) {
+ return;
+ }
+ try {
+ mVirtualMachine.stop();
+ String name = mVirtualMachine.getName();
+ VirtualMachineManager vmm = getSystemService(VirtualMachineManager.class);
+ vmm.delete(name);
+ mVirtualMachine = null;
+ } catch (VirtualMachineException e) {
+ Log.e(TAG, "Failed to stop " + mVirtualMachine, e);
+ }
+ }
+
+ public ITestService startVm(VirtualMachineDescriptor vmDesc) throws Exception {
+ // Cleanup VM left from the previous test.
+ deleteVm();
+
+ VirtualMachineManager vmm = getSystemService(VirtualMachineManager.class);
+
+ // Add random uuid to make sure that different tests that bind to this service don't trip
+ // over each other.
+ String vmName = "imported_vm" + UUID.randomUUID();
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ VirtualMachineCallback callback =
+ new VirtualMachineCallback() {
+
+ @Override
+ public void onPayloadStarted(VirtualMachine vm) {
+ // Ignored
+ }
+
+ @Override
+ public void onPayloadReady(VirtualMachine vm) {
+ latch.countDown();
+ }
+
+ @Override
+ public void onPayloadFinished(VirtualMachine vm, int exitCode) {
+ // Ignored
+ }
+
+ @Override
+ public void onError(VirtualMachine vm, int errorCode, String message) {
+ throw new RuntimeException(
+ "VM failed with error " + errorCode + " : " + message);
+ }
+
+ @Override
+ public void onStopped(VirtualMachine vm, int reason) {
+ // Ignored
+ }
+ };
+
+ mVirtualMachine = vmm.importFromDescriptor(vmName, vmDesc);
+ mVirtualMachine.setCallback(getMainExecutor(), callback);
+
+ Log.i(TAG, "Starting VM " + vmName);
+ mVirtualMachine.run();
+ if (!latch.await(1, TimeUnit.MINUTES)) {
+ throw new TimeoutException("Timed out starting VM");
+ }
+
+ Log.i(
+ TAG,
+ "Payload is ready, connecting to the vsock service at port "
+ + ITestService.SERVICE_PORT);
+ ITestService testService =
+ ITestService.Stub.asInterface(
+ mVirtualMachine.connectToVsockServer(ITestService.SERVICE_PORT));
+ return new RemoteTestServiceDelegate(testService);
+ }
+
+ final class ServiceImpl extends IVmShareTestService.Stub {
+
+ @Override
+ public ITestService startVm(VirtualMachineDescriptor vmDesc) {
+ Log.i(TAG, "startVm binder call received");
+ try {
+ return VmShareServiceImpl.this.startVm(vmDesc);
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to startVm", e);
+ throw new IllegalStateException("Failed to startVm", e);
+ }
+ }
+ }
+
+ private static class RemoteTestServiceDelegate extends ITestService.Stub {
+
+ private final ITestService mServiceInVm;
+
+ private RemoteTestServiceDelegate(ITestService serviceInVm) {
+ mServiceInVm = serviceInVm;
+ }
+
+ @Override
+ public int addInteger(int a, int b) throws RemoteException {
+ return mServiceInVm.addInteger(a, b);
+ }
+
+ @Override
+ public String readProperty(String prop) throws RemoteException {
+ throw new UnsupportedOperationException("Not supported");
+ }
+
+ @Override
+ public byte[] insecurelyExposeVmInstanceSecret() throws RemoteException {
+ throw new UnsupportedOperationException("Not supported");
+ }
+
+ @Override
+ public byte[] insecurelyExposeAttestationCdi() throws RemoteException {
+ throw new UnsupportedOperationException("Not supported");
+ }
+
+ @Override
+ public byte[] getBcc() throws RemoteException {
+ throw new UnsupportedOperationException("Not supported");
+ }
+
+ @Override
+ public String getApkContentsPath() throws RemoteException {
+ throw new UnsupportedOperationException("Not supported");
+ }
+
+ @Override
+ public String getEncryptedStoragePath() throws RemoteException {
+ throw new UnsupportedOperationException("Not supported");
+ }
+
+ @Override
+ public void runEchoReverseServer() throws RemoteException {
+ throw new UnsupportedOperationException("Not supported");
+ }
+
+ @Override
+ public String[] getEffectiveCapabilities() throws RemoteException {
+ throw new UnsupportedOperationException("Not supported");
+ }
+
+ @Override
+ public void writeToFile(String content, String path) throws RemoteException {
+ throw new UnsupportedOperationException("Not supported");
+ }
+
+ @Override
+ public String readFromFile(String path) throws RemoteException {
+ // TODO(b/259384440): implement for the VM share test including trusted storage.
+ throw new UnsupportedOperationException("Not supported");
+ }
+
+ @Override
+ public void quit() throws RemoteException {
+ throw new UnsupportedOperationException("Not supported");
+ }
+ }
+}