Support config-less VMs

Update the Java API to allow the config file to be omitted. Instead,
the binary path can be specified explicitly.

Remove certs from VirtualMachineConfig; they were only used for
compatibility checking, and running a VM with a different APK
(regardless of singing certs) isn't supported. Require whether the VM
is protected to be specified explicitly, to avoid accidents.

Modify the config serialization format & bump the version.

Modify demo + test clients to match.

Refactor the tests to split off the extra APK test (which requires a
config file), minimizing code duplication.

Migrate one success test to run without config file.

Bug: 243513572
Test: atest MicrodroidTests
Change-Id: I6b195d4daa9c8132e1f71a04d2253f538edcbe4a
diff --git a/demo/java/com/android/microdroid/demo/MainActivity.java b/demo/java/com/android/microdroid/demo/MainActivity.java
index 1fdce03..7624876 100644
--- a/demo/java/com/android/microdroid/demo/MainActivity.java
+++ b/demo/java/com/android/microdroid/demo/MainActivity.java
@@ -258,7 +258,9 @@
 
             try {
                 VirtualMachineConfig.Builder builder =
-                        new VirtualMachineConfig.Builder(getApplication(), "assets/vm_config.json");
+                        new VirtualMachineConfig.Builder(getApplication());
+                builder.setPayloadConfigPath("assets/vm_config.json");
+                builder.setProtectedVm(true);
                 if (debug) {
                     builder.setDebugLevel(VirtualMachineConfig.DEBUG_LEVEL_FULL);
                 }
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachine.java b/javalib/src/android/system/virtualmachine/VirtualMachine.java
index 2b4d185..bdec164 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachine.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachine.java
@@ -78,6 +78,7 @@
 import java.nio.file.FileAlreadyExistsException;
 import java.nio.file.Files;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -791,11 +792,29 @@
 
     @Override
     public String toString() {
-        return "VirtualMachine("
-                + "name:" + getName() + ", "
-                + "config:" + getConfig().getPayloadConfigPath() + ", "
-                + "package: " + mPackageName
-                + ")";
+        VirtualMachineConfig config = getConfig();
+        String payloadConfigPath = config.getPayloadConfigPath();
+        String payloadBinaryPath = config.getPayloadBinaryPath();
+
+        StringBuilder result = new StringBuilder();
+        result.append("VirtualMachine(")
+                .append("name:")
+                .append(getName())
+                .append(", ");
+        if (payloadBinaryPath != null) {
+            result.append("payload:")
+                    .append(payloadBinaryPath)
+                    .append(", ");
+        }
+        if (payloadConfigPath != null) {
+            result.append("config:")
+                    .append(payloadConfigPath)
+                    .append(", ");
+        }
+        result.append("package: ")
+                .append(mPackageName)
+                .append(")");
+        return result.toString();
     }
 
     private static List<String> parseExtraApkListFromPayloadConfig(JsonReader reader)
@@ -841,10 +860,14 @@
     private static List<ExtraApkSpec> setupExtraApks(
             @NonNull Context context, @NonNull VirtualMachineConfig config, @NonNull File vmDir)
             throws VirtualMachineException {
+        String configPath = config.getPayloadConfigPath();
+        if (configPath == null) {
+            return Collections.emptyList();
+        }
         try {
             ZipFile zipFile = new ZipFile(context.getPackageCodePath());
             InputStream inputStream =
-                    zipFile.getInputStream(zipFile.getEntry(config.getPayloadConfigPath()));
+                    zipFile.getInputStream(zipFile.getEntry(configPath));
             List<String> apkList =
                     parseExtraApkListFromPayloadConfig(
                             new JsonReader(new InputStreamReader(inputStream)));
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
index 7f41874..3061f65 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -18,18 +18,15 @@
 
 import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
 
-import static java.util.Objects.requireNonNull;
-
 import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.content.Context;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.PackageInfoFlags;
-import android.content.pm.Signature;
 import android.os.ParcelFileDescriptor;
 import android.os.PersistableBundle;
 import android.sysprop.HypervisorProperties;
 import android.system.virtualizationservice.VirtualMachineAppConfig;
+import android.system.virtualizationservice.VirtualMachinePayloadConfig;
 
 import java.io.File;
 import java.io.FileNotFoundException;
@@ -38,9 +35,7 @@
 import java.io.OutputStream;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
+import java.util.Objects;
 
 /**
  * Represents a configuration of a virtual machine. A configuration consists of hardware
@@ -51,11 +46,11 @@
  */
 public final class VirtualMachineConfig {
     // These defines the schema of the config file persisted on disk.
-    private static final int VERSION = 1;
+    private static final int VERSION = 2;
     private static final String KEY_VERSION = "version";
-    private static final String KEY_CERTS = "certs";
     private static final String KEY_APKPATH = "apkPath";
     private static final String KEY_PAYLOADCONFIGPATH = "payloadConfigPath";
+    private static final String KEY_PAYLOADBINARYPATH = "payloadBinaryPath";
     private static final String KEY_DEBUGLEVEL = "debugLevel";
     private static final String KEY_PROTECTED_VM = "protectedVm";
     private static final String KEY_MEMORY_MIB = "memoryMib";
@@ -63,7 +58,6 @@
 
     // Paths to the APK file of this application.
     @NonNull private final String mApkPath;
-    @NonNull private final Signature[] mCerts;
 
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
@@ -116,21 +110,26 @@
     private final int mNumCpus;
 
     /**
-     * Path within the APK to the payload config file that defines software aspects of this config.
+     * Path within the APK to the payload config file that defines software aspects of the VM.
      */
-    @NonNull private final String mPayloadConfigPath;
+    @Nullable private final String mPayloadConfigPath;
+
+    /**
+     * Path within the APK to the payload binary file that will be executed within the VM.
+     */
+    @Nullable private final String mPayloadBinaryPath;
 
     private VirtualMachineConfig(
             @NonNull String apkPath,
-            @NonNull Signature[] certs,
-            @NonNull String payloadConfigPath,
+            @Nullable String payloadConfigPath,
+            @Nullable String payloadBinaryPath,
             @DebugLevel int debugLevel,
             boolean protectedVm,
             int memoryMib,
             int numCpus) {
-        mApkPath = apkPath;
-        mCerts = certs;
+        mApkPath = Objects.requireNonNull(apkPath);
         mPayloadConfigPath = payloadConfigPath;
+        mPayloadBinaryPath = payloadBinaryPath;
         mDebugLevel = debugLevel;
         mProtectedVm = protectedVm;
         mMemoryMib = memoryMib;
@@ -142,37 +141,33 @@
     static VirtualMachineConfig from(@NonNull InputStream input)
             throws IOException, VirtualMachineException {
         PersistableBundle b = PersistableBundle.readFromStream(input);
-        final int version = b.getInt(KEY_VERSION);
+        int version = b.getInt(KEY_VERSION);
         if (version > VERSION) {
             throw new VirtualMachineException("Version too high");
         }
-        final String apkPath = b.getString(KEY_APKPATH);
+        String apkPath = b.getString(KEY_APKPATH);
         if (apkPath == null) {
             throw new VirtualMachineException("No apkPath");
         }
-        final String[] certStrings = b.getStringArray(KEY_CERTS);
-        if (certStrings == null || certStrings.length == 0) {
-            throw new VirtualMachineException("No certs");
+        String payloadBinaryPath = b.getString(KEY_PAYLOADBINARYPATH);
+        String payloadConfigPath = null;
+        if (payloadBinaryPath == null) {
+            payloadConfigPath = b.getString(KEY_PAYLOADCONFIGPATH);
+            if (payloadConfigPath == null) {
+                throw new VirtualMachineException("No payloadBinaryPath");
+            }
         }
-        List<Signature> certList = new ArrayList<>();
-        for (String s : certStrings) {
-            certList.add(new Signature(s));
-        }
-        Signature[] certs = certList.toArray(new Signature[0]);
-        final String payloadConfigPath = b.getString(KEY_PAYLOADCONFIGPATH);
-        if (payloadConfigPath == null) {
-            throw new VirtualMachineException("No payloadConfigPath");
-        }
-        @DebugLevel final int debugLevel = b.getInt(KEY_DEBUGLEVEL);
+        @DebugLevel int debugLevel = b.getInt(KEY_DEBUGLEVEL);
         if (debugLevel != DEBUG_LEVEL_NONE && debugLevel != DEBUG_LEVEL_APP_ONLY
                 && debugLevel != DEBUG_LEVEL_FULL) {
             throw new VirtualMachineException("Invalid debugLevel: " + debugLevel);
         }
-        final boolean protectedVm = b.getBoolean(KEY_PROTECTED_VM);
-        final int memoryMib = b.getInt(KEY_MEMORY_MIB);
-        final int numCpus = b.getInt(KEY_NUM_CPUS);
-        return new VirtualMachineConfig(apkPath, certs, payloadConfigPath, debugLevel, protectedVm,
-                memoryMib, numCpus);
+        boolean protectedVm = b.getBoolean(KEY_PROTECTED_VM);
+        int memoryMib = b.getInt(KEY_MEMORY_MIB);
+        int numCpus = b.getInt(KEY_NUM_CPUS);
+
+        return new VirtualMachineConfig(apkPath, payloadConfigPath, payloadBinaryPath, debugLevel,
+                protectedVm, memoryMib, numCpus);
     }
 
     /** Persists this config to a stream, for example a file. */
@@ -180,13 +175,8 @@
         PersistableBundle b = new PersistableBundle();
         b.putInt(KEY_VERSION, VERSION);
         b.putString(KEY_APKPATH, mApkPath);
-        List<String> certList = new ArrayList<>();
-        for (Signature cert : mCerts) {
-            certList.add(cert.toCharsString());
-        }
-        String[] certs = certList.toArray(new String[0]);
-        b.putStringArray(KEY_CERTS, certs);
         b.putString(KEY_PAYLOADCONFIGPATH, mPayloadConfigPath);
+        b.putString(KEY_PAYLOADBINARYPATH, mPayloadBinaryPath);
         b.putInt(KEY_DEBUGLEVEL, mDebugLevel);
         b.putBoolean(KEY_PROTECTED_VM, mProtectedVm);
         b.putInt(KEY_NUM_CPUS, mNumCpus);
@@ -201,12 +191,23 @@
      *
      * @hide
      */
-    @NonNull
+    @Nullable
     public String getPayloadConfigPath() {
         return mPayloadConfigPath;
     }
 
     /**
+     * Returns the path within the APK to the payload binary file that will be executed within the
+     * VM.
+     *
+     * @hide
+     */
+    @Nullable
+    public String getPayloadBinaryPath() {
+        return mPayloadBinaryPath;
+    }
+
+    /**
      * Returns the debug level for the VM.
      *
      * @hide
@@ -247,24 +248,17 @@
     /**
      * Tests if this config is compatible with other config. Being compatible means that the configs
      * can be interchangeably used for the same virtual machine. Compatible changes includes the
-     * number of CPUs and the size of the RAM, and change of the payload as long as the payload is
-     * signed by the same signer. All other changes (e.g. using a payload from a different signer,
+     * number of CPUs and the size of the RAM. All other changes (e.g. using a different payload,
      * change of the debug mode, etc.) are considered as incompatible.
      *
      * @hide
      */
     public boolean isCompatibleWith(@NonNull VirtualMachineConfig other) {
-        if (!Arrays.equals(this.mCerts, other.mCerts)) {
-            return false;
-        }
-        if (this.mDebugLevel != other.mDebugLevel) {
-            // TODO(jiyong): should we treat APP_ONLY and FULL the same?
-            return false;
-        }
-        if (this.mProtectedVm != other.mProtectedVm) {
-            return false;
-        }
-        return true;
+        return this.mDebugLevel == other.mDebugLevel
+                && this.mProtectedVm == other.mProtectedVm
+                && Objects.equals(this.mPayloadConfigPath, other.mPayloadConfigPath)
+                && Objects.equals(this.mPayloadBinaryPath, other.mPayloadBinaryPath)
+                && this.mApkPath.equals(other.mApkPath);
     }
 
     /**
@@ -273,10 +267,19 @@
      * service doesn't accept paths as it might not have permission to open app-owned files and that
      * could be abused to run a VM with software that the calling application doesn't own.
      */
-    /* package */ VirtualMachineAppConfig toParcel() throws FileNotFoundException {
+    VirtualMachineAppConfig toParcel() throws FileNotFoundException {
         VirtualMachineAppConfig parcel = new VirtualMachineAppConfig();
         parcel.apk = ParcelFileDescriptor.open(new File(mApkPath), MODE_READ_ONLY);
-        parcel.payload = VirtualMachineAppConfig.Payload.configPath(mPayloadConfigPath);
+        if (mPayloadBinaryPath != null) {
+            VirtualMachinePayloadConfig payloadConfig = new VirtualMachinePayloadConfig();
+            payloadConfig.payloadPath = mPayloadBinaryPath;
+            payloadConfig.args = new String[]{};
+            parcel.payload =
+                    VirtualMachineAppConfig.Payload.payloadConfig(payloadConfig);
+        } else {
+            parcel.payload =
+                    VirtualMachineAppConfig.Payload.configPath(mPayloadConfigPath);
+        }
         switch (mDebugLevel) {
             case DEBUG_LEVEL_APP_ONLY:
                 parcel.debugLevel = VirtualMachineAppConfig.DebugLevel.APP_ONLY;
@@ -304,27 +307,95 @@
      */
     public static final class Builder {
         private final Context mContext;
-        private final String mPayloadConfigPath;
+        @Nullable private String mPayloadConfigPath;
+        @Nullable private String mPayloadBinaryPath;
         @DebugLevel private int mDebugLevel;
         private boolean mProtectedVm;
+        private boolean mProtectedVmSet;
         private int mMemoryMib;
         private int mNumCpus;
 
         /**
-         * Creates a builder for the given context (APK), and the payload config file in APK.
+         * Creates a builder for the given context (APK).
          *
          * @hide
          */
-        public Builder(@NonNull Context context, @NonNull String payloadConfigPath) {
-            mContext = requireNonNull(context, "context must not be null");
-            mPayloadConfigPath = requireNonNull(payloadConfigPath,
-                    "payloadConfigPath must not be null");
+        public Builder(@NonNull Context context) {
+            mContext = Objects.requireNonNull(context);
             mDebugLevel = DEBUG_LEVEL_NONE;
-            mProtectedVm = false;
             mNumCpus = 1;
         }
 
         /**
+         * Builds an immutable {@link VirtualMachineConfig}
+         *
+         * @hide
+         */
+        @NonNull
+        public VirtualMachineConfig build() {
+            final String apkPath = mContext.getPackageCodePath();
+
+            final int availableCpus = Runtime.getRuntime().availableProcessors();
+            if (mNumCpus < 1 || mNumCpus > availableCpus) {
+                throw new IllegalArgumentException("Number of vCPUs (" + mNumCpus + ") is out of "
+                        + "range [1, " + availableCpus + "]");
+            }
+
+            if (mPayloadBinaryPath == null) {
+                if (mPayloadConfigPath == null) {
+                    throw new IllegalStateException("payloadBinaryPath must be set");
+                }
+            } else {
+                if (mPayloadConfigPath != null) {
+                    throw new IllegalStateException(
+                            "payloadBinaryPath and payloadConfigPath may not both be set");
+                }
+            }
+
+            if (!mProtectedVmSet) {
+                throw new IllegalStateException("protectedVm must be set explicitly");
+            }
+
+            if (mProtectedVm
+                    && !HypervisorProperties.hypervisor_protected_vm_supported().orElse(false)) {
+                throw new UnsupportedOperationException(
+                        "Protected VMs are not supported on this device.");
+            }
+            if (!mProtectedVm && !HypervisorProperties.hypervisor_vm_supported().orElse(false)) {
+                throw new UnsupportedOperationException(
+                        "Unprotected VMs are not supported on this device.");
+            }
+
+            return new VirtualMachineConfig(
+                    apkPath, mPayloadConfigPath, mPayloadBinaryPath, mDebugLevel, mProtectedVm,
+                    mMemoryMib, mNumCpus);
+        }
+
+        /**
+         * Sets the path within the APK to the payload config file that defines software aspects
+         * of the VM.
+         *
+         * @hide
+         */
+        @NonNull
+        public Builder setPayloadConfigPath(@NonNull String payloadConfigPath) {
+            mPayloadConfigPath = Objects.requireNonNull(payloadConfigPath);
+            return this;
+        }
+
+        /**
+         * Sets the path within the APK to the payload binary file that will be executed within
+         * the VM.
+         *
+         * @hide
+         */
+        @NonNull
+        public Builder setPayloadBinaryPath(@NonNull String payloadBinaryPath) {
+            mPayloadBinaryPath = Objects.requireNonNull(payloadBinaryPath);
+            return this;
+        }
+
+        /**
          * Sets the debug level
          *
          * @hide
@@ -336,13 +407,15 @@
         }
 
         /**
-         *  Sets whether to protect the VM memory from the host. Defaults to false.
+         * Sets whether to protect the VM memory from the host. No default is provided, this
+         * must be set explicitly.
          *
          * @hide
          */
         @NonNull
         public Builder setProtectedVm(boolean protectedVm) {
             mProtectedVm = protectedVm;
+            mProtectedVmSet = true;
             return this;
         }
 
@@ -368,47 +441,5 @@
             mNumCpus = num;
             return this;
         }
-
-        /**
-         * Builds an immutable {@link VirtualMachineConfig}
-         *
-         * @hide
-         */
-        @NonNull
-        public VirtualMachineConfig build() {
-            final String apkPath = mContext.getPackageCodePath();
-            final String packageName = mContext.getPackageName();
-            Signature[] certs;
-            try {
-                certs = mContext.getPackageManager()
-                        .getPackageInfo(packageName,
-                                PackageInfoFlags.of(PackageManager.GET_SIGNING_CERTIFICATES))
-                        .signingInfo
-                        .getSigningCertificateHistory();
-            } catch (PackageManager.NameNotFoundException e) {
-                // This cannot happen as `packageName` is from this app.
-                throw new RuntimeException(e);
-            }
-
-            final int availableCpus = Runtime.getRuntime().availableProcessors();
-            if (mNumCpus < 1 || mNumCpus > availableCpus) {
-                throw new IllegalArgumentException("Number of vCPUs (" + mNumCpus + ") is out of "
-                        + "range [1, " + availableCpus + "]");
-            }
-
-            if (mProtectedVm
-                    && !HypervisorProperties.hypervisor_protected_vm_supported().orElse(false)) {
-                throw new UnsupportedOperationException(
-                        "Protected VMs are not supported on this device.");
-            }
-            if (!mProtectedVm && !HypervisorProperties.hypervisor_vm_supported().orElse(false)) {
-                throw new UnsupportedOperationException(
-                        "Unprotected VMs are not supported on this device.");
-            }
-
-            return new VirtualMachineConfig(
-                    apkPath, certs, mPayloadConfigPath, mDebugLevel, mProtectedVm, mMemoryMib,
-                    mNumCpus);
-        }
     }
 }
diff --git a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
index 2856a30..2deeb70 100644
--- a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
+++ b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
@@ -77,10 +77,12 @@
             return mContext;
         }
 
-        /** Create a new VirtualMachineConfig.Builder with the parameterized protection mode. */
+        public VirtualMachineConfig.Builder newVmConfigBuilder() {
+            return new VirtualMachineConfig.Builder(mContext).setProtectedVm(mProtectedVm);
+        }
+
         public VirtualMachineConfig.Builder newVmConfigBuilder(String payloadConfigPath) {
-            return new VirtualMachineConfig.Builder(mContext, payloadConfigPath)
-                        .setProtectedVm(mProtectedVm);
+            return newVmConfigBuilder().setPayloadConfigPath(payloadConfigPath);
         }
 
         /**
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 707930f..b0ec359 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -87,78 +87,35 @@
             "9.17/C-2-1"
     })
     public void connectToVmService() throws Exception {
-        assume()
-            .withMessage("SKip on 5.4 kernel. b/218303240")
-            .that(KERNEL_VERSION)
-            .isNotEqualTo("5.4");
+        assumeSupportedKernel();
 
-        VirtualMachineConfig.Builder builder =
-                mInner.newVmConfigBuilder("assets/vm_config_extra_apk.json");
-        if (Build.SUPPORTED_ABIS.length > 0) {
-            String primaryAbi = Build.SUPPORTED_ABIS[0];
-            switch(primaryAbi) {
-                case "x86_64":
-                    builder.setMemoryMib(MIN_MEM_X86_64);
-                    break;
-                case "arm64-v8a":
-                    builder.setMemoryMib(MIN_MEM_ARM64);
-                    break;
-            }
-        }
-        VirtualMachineConfig config = builder.build();
+        VirtualMachineConfig config = mInner.newVmConfigBuilder()
+                .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
+                .setMemoryMib(minMemoryRequired())
+                .build();
         VirtualMachine vm = mInner.forceCreateNewVirtualMachine("test_vm_extra_apk", config);
 
-        class TestResults {
-            Exception mException;
-            Integer mAddInteger;
-            String mAppRunProp;
-            String mSublibRunProp;
-            String mExtraApkTestProp;
-        }
-        final CompletableFuture<Boolean> payloadStarted = new CompletableFuture<>();
-        final CompletableFuture<Boolean> payloadReady = new CompletableFuture<>();
-        final TestResults testResults = new TestResults();
-        VmEventListener listener =
-                new VmEventListener() {
-                    private void testVMService(VirtualMachine vm) {
-                        try {
-                            ITestService testService = ITestService.Stub.asInterface(
-                                    vm.connectToVsockServer(ITestService.SERVICE_PORT));
-                            testResults.mAddInteger = testService.addInteger(123, 456);
-                            testResults.mAppRunProp =
-                                    testService.readProperty("debug.microdroid.app.run");
-                            testResults.mSublibRunProp =
-                                    testService.readProperty("debug.microdroid.app.sublib.run");
-                            testResults.mExtraApkTestProp =
-                                    testService.readProperty("debug.microdroid.test.extra_apk");
-                        } catch (Exception e) {
-                            testResults.mException = e;
-                        }
-                    }
-
-                    @Override
-                    public void onPayloadReady(VirtualMachine vm) {
-                        Log.i(TAG, "onPayloadReady");
-                        payloadReady.complete(true);
-                        testVMService(vm);
-                        forceStop(vm);
-                    }
-
-                    @Override
-                    public void onPayloadStarted(VirtualMachine vm, ParcelFileDescriptor stream) {
-                        Log.i(TAG, "onPayloadStarted");
-                        payloadStarted.complete(true);
-                        logVmOutput(TAG, new FileInputStream(stream.getFileDescriptor()),
-                                "Payload");
-                    }
-                };
-        listener.runToFinish(TAG, vm);
-        assertThat(payloadStarted.getNow(false)).isTrue();
-        assertThat(payloadReady.getNow(false)).isTrue();
+        TestResults testResults = runVmTestService(vm);
         assertThat(testResults.mException).isNull();
         assertThat(testResults.mAddInteger).isEqualTo(123 + 456);
         assertThat(testResults.mAppRunProp).isEqualTo("true");
         assertThat(testResults.mSublibRunProp).isEqualTo("true");
+    }
+
+    @Test
+    @CddTest(requirements = {
+            "9.17/C-1-1",
+            "9.17/C-2-1"
+    })
+    public void extraApk() throws Exception {
+        assumeSupportedKernel();
+
+        VirtualMachineConfig config = mInner.newVmConfigBuilder("assets/vm_config_extra_apk.json")
+                .setMemoryMib(minMemoryRequired())
+                .build();
+        VirtualMachine vm = mInner.forceCreateNewVirtualMachine("test_vm_extra_apk", config);
+
+        TestResults testResults = runVmTestService(vm);
         assertThat(testResults.mExtraApkTestProp).isEqualTo("PASS");
     }
 
@@ -198,10 +155,7 @@
             "9.17/C-2-7"
     })
     public void changingDebugLevelInvalidatesVmIdentity() throws Exception {
-        assume()
-            .withMessage("SKip on 5.4 kernel. b/218303240")
-            .that(KERNEL_VERSION)
-            .isNotEqualTo("5.4");
+        assumeSupportedKernel();
 
         VirtualMachineConfig.Builder builder = mInner.newVmConfigBuilder("assets/vm_config.json");
         VirtualMachineConfig normalConfig = builder.setDebugLevel(DEBUG_LEVEL_NONE).build();
@@ -263,10 +217,7 @@
             "9.17/C-2-7"
     })
     public void instancesOfSameVmHaveDifferentCdis() throws Exception {
-        assume()
-            .withMessage("SKip on 5.4 kernel. b/218303240")
-            .that(KERNEL_VERSION)
-            .isNotEqualTo("5.4");
+        assumeSupportedKernel();
 
         VirtualMachineConfig normalConfig = mInner.newVmConfigBuilder("assets/vm_config.json")
                 .setDebugLevel(DEBUG_LEVEL_FULL)
@@ -290,10 +241,7 @@
             "9.17/C-2-7"
     })
     public void sameInstanceKeepsSameCdis() throws Exception {
-        assume()
-            .withMessage("SKip on 5.4 kernel. b/218303240")
-            .that(KERNEL_VERSION)
-            .isNotEqualTo("5.4");
+        assumeSupportedKernel();
 
         VirtualMachineConfig normalConfig = mInner.newVmConfigBuilder("assets/vm_config.json")
                 .setDebugLevel(DEBUG_LEVEL_FULL)
@@ -314,10 +262,7 @@
             "9.17/C-2-7"
     })
     public void bccIsSuperficiallyWellFormed() throws Exception {
-        assume()
-            .withMessage("SKip on 5.4 kernel. b/218303240")
-            .that(KERNEL_VERSION)
-            .isNotEqualTo("5.4");
+        assumeSupportedKernel();
 
         VirtualMachineConfig normalConfig = mInner.newVmConfigBuilder("assets/vm_config.json")
                 .setDebugLevel(DEBUG_LEVEL_FULL)
@@ -479,4 +424,76 @@
 
         assertThat(vm).isNotEqualTo(newVm);
     }
+
+    private int minMemoryRequired() {
+        if (Build.SUPPORTED_ABIS.length > 0) {
+            String primaryAbi = Build.SUPPORTED_ABIS[0];
+            switch (primaryAbi) {
+                case "x86_64":
+                    return MIN_MEM_X86_64;
+                case "arm64-v8a":
+                    return MIN_MEM_ARM64;
+            }
+        }
+        return 0;
+    }
+
+    private void assumeSupportedKernel() {
+        assume()
+                .withMessage("Skip on 5.4 kernel. b/218303240")
+                .that(KERNEL_VERSION)
+                .isNotEqualTo("5.4");
+    }
+
+    static class TestResults {
+        Exception mException;
+        Integer mAddInteger;
+        String mAppRunProp;
+        String mSublibRunProp;
+        String mExtraApkTestProp;
+    }
+
+    private TestResults runVmTestService(VirtualMachine vm) throws Exception {
+        CompletableFuture<Boolean> payloadStarted = new CompletableFuture<>();
+        CompletableFuture<Boolean> payloadReady = new CompletableFuture<>();
+        TestResults testResults = new TestResults();
+        VmEventListener listener =
+                new VmEventListener() {
+                    private void testVMService(VirtualMachine vm) {
+                        try {
+                            ITestService testService = ITestService.Stub.asInterface(
+                                    vm.connectToVsockServer(ITestService.SERVICE_PORT));
+                            testResults.mAddInteger = testService.addInteger(123, 456);
+                            testResults.mAppRunProp =
+                                    testService.readProperty("debug.microdroid.app.run");
+                            testResults.mSublibRunProp =
+                                    testService.readProperty("debug.microdroid.app.sublib.run");
+                            testResults.mExtraApkTestProp =
+                                    testService.readProperty("debug.microdroid.test.extra_apk");
+                        } catch (Exception e) {
+                            testResults.mException = e;
+                        }
+                    }
+
+                    @Override
+                    public void onPayloadReady(VirtualMachine vm) {
+                        Log.i(TAG, "onPayloadReady");
+                        payloadReady.complete(true);
+                        testVMService(vm);
+                        forceStop(vm);
+                    }
+
+                    @Override
+                    public void onPayloadStarted(VirtualMachine vm, ParcelFileDescriptor stream) {
+                        Log.i(TAG, "onPayloadStarted");
+                        payloadStarted.complete(true);
+                        logVmOutput(TAG, new FileInputStream(stream.getFileDescriptor()),
+                                "Payload");
+                    }
+                };
+        listener.runToFinish(TAG, vm);
+        assertThat(payloadStarted.getNow(false)).isTrue();
+        assertThat(payloadReady.getNow(false)).isTrue();
+        return testResults;
+    }
 }
diff --git a/tests/testapk/src/native/testbinary.cpp b/tests/testapk/src/native/testbinary.cpp
index b4fee86..c8d07a3 100644
--- a/tests/testapk/src/native/testbinary.cpp
+++ b/tests/testapk/src/native/testbinary.cpp
@@ -181,7 +181,7 @@
     setvbuf(stdout, nullptr, _IONBF, 0);
     setvbuf(stderr, nullptr, _IONBF, 0);
 
-    if (strcmp(argv[1], "crash") == 0) {
+    if (argc >= 2 && strcmp(argv[1], "crash") == 0) {
         printf("test crash!!!!\n");
         abort();
     }