Config file requires custom VM permission

Modify VS to require the USE_CUSTOM_VIRTUAL_MACHINE permission if a
config file is specified.

Modify the tests to grant the necessary permissions at runtime rather
than via AndroidTest.xml. Make use of a config file explicit, and only
do so (and grant the custom VM permission) for the tests that need it.

Moved the existing permission test to a device test and added a new
one for the custom VM permission. That failed unexpectedly, so I fixed
the way we were reporting the exception.

Other minor tidying up. Renamed MicrodroidTestCase to
MicrodroidHostTests because it kept confusing me.

Bug: 243513572
Test: atest MicrodroidTests MicrodroidHostTests
Change-Id: Ie67e7ed214dc9c95e453ca1fcc38a1b18d4f5f88
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachine.java b/javalib/src/android/system/virtualmachine/VirtualMachine.java
index 61c41a7..81f97f3 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachine.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachine.java
@@ -124,6 +124,13 @@
             "android.permission.MANAGE_VIRTUAL_MACHINE";
 
     /**
+     * The permission needed to create a virtual machine with more advanced configuration options.
+     */
+    public static final String USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION =
+            "android.permission.USE_CUSTOM_VIRTUAL_MACHINE";
+
+
+    /**
      * Status of a virtual machine
      *
      * @hide
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
index 9f62579..1da17b9 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -21,6 +21,7 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
 import android.content.Context;
 import android.os.ParcelFileDescriptor;
 import android.os.PersistableBundle;
@@ -376,6 +377,7 @@
          *
          * @hide
          */
+        @RequiresPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION)
         @NonNull
         public Builder setPayloadConfigPath(@NonNull String payloadConfigPath) {
             mPayloadConfigPath = Objects.requireNonNull(payloadConfigPath);
@@ -383,8 +385,8 @@
         }
 
         /**
-         * Sets the path within the APK to the payload binary file that will be executed within
-         * the VM.
+         * Sets the path within the {@code lib/<ABI>} directory of the APK to the payload binary
+         * file that will be executed within the VM.
          *
          * @hide
          */
diff --git a/tests/benchmark/AndroidManifest.xml b/tests/benchmark/AndroidManifest.xml
index ff18130..c39b91c 100644
--- a/tests/benchmark/AndroidManifest.xml
+++ b/tests/benchmark/AndroidManifest.xml
@@ -16,6 +16,7 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
       package="com.android.microdroid.benchmark">
     <uses-permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE" />
+    <uses-permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE" />
     <application>
         <uses-library android:name="android.system.virtualmachine" android:required="false" />
     </application>
diff --git a/tests/benchmark/AndroidTest.xml b/tests/benchmark/AndroidTest.xml
index 4949d22..0214cd9 100644
--- a/tests/benchmark/AndroidTest.xml
+++ b/tests/benchmark/AndroidTest.xml
@@ -13,7 +13,7 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<configuration description="Runs sample instrumentation test.">
+<configuration description="Runs Microdroid benchmarks.">
     <option name="config-descriptor:metadata" key="component" value="security" />
     <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
     <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
@@ -25,11 +25,6 @@
     <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer">
         <option name="force-root" value="true" />
     </target_preparer>
-    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
-      <option
-        name="run-command"
-        value="pm grant com.android.microdroid.benchmark android.permission.MANAGE_VIRTUAL_MACHINE" />
-    </target_preparer>
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="com.android.microdroid.benchmark" />
         <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
diff --git a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
index b1a1160..23d546d 100644
--- a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
+++ b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
@@ -80,20 +80,23 @@
 
     @Before
     public void setup() {
+        grantPermission(VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION);
+        grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
         prepareTestSetup(mProtectedVm);
         mInstrumentation = getInstrumentation();
     }
 
     private boolean canBootMicrodroidWithMemory(int mem)
             throws VirtualMachineException, InterruptedException, IOException {
-        final int trialCount = 5;
+        VirtualMachineConfig normalConfig = mInner.newVmConfigBuilder()
+                .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
+                .setDebugLevel(DEBUG_LEVEL_NONE)
+                .setMemoryMib(mem)
+                .build();
 
         // returns true if succeeded at least once.
+        final int trialCount = 5;
         for (int i = 0; i < trialCount; i++) {
-            VirtualMachineConfig.Builder builder =
-                    mInner.newVmConfigBuilder("assets/vm_config.json");
-            VirtualMachineConfig normalConfig =
-                    builder.setDebugLevel(DEBUG_LEVEL_NONE).setMemoryMib(mem).build();
             mInner.forceCreateNewVirtualMachine("test_vm_minimum_memory", normalConfig);
 
             if (tryBootVm(TAG, "test_vm_minimum_memory").payloadStarted) return true;
@@ -144,7 +147,8 @@
         for (int i = 0; i < trialCount; i++) {
 
             // To grab boot events from log, set debug mode to FULL
-            VirtualMachineConfig normalConfig = mInner.newVmConfigBuilder("assets/vm_config.json")
+            VirtualMachineConfig normalConfig = mInner.newVmConfigBuilder()
+                    .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
                     .setDebugLevel(DEBUG_LEVEL_FULL)
                     .setMemoryMib(256)
                     .build();
@@ -191,7 +195,8 @@
 
     @Test
     public void testVsockTransferFromHostToVM() throws Exception {
-        VirtualMachineConfig config = mInner.newVmConfigBuilder("assets/vm_config_io.json")
+        VirtualMachineConfig config = mInner.newVmConfigBuilder()
+                .setPayloadConfigPath("assets/vm_config_io.json")
                 .setDebugLevel(DEBUG_LEVEL_FULL)
                 .build();
         List<Double> transferRates = new ArrayList<>(IO_TEST_TRIAL_COUNT);
@@ -217,7 +222,8 @@
     }
 
     private void testVirtioBlkReadRate(boolean isRand) throws Exception {
-        VirtualMachineConfig config = mInner.newVmConfigBuilder("assets/vm_config_io.json")
+        VirtualMachineConfig config = mInner.newVmConfigBuilder()
+                .setPayloadConfigPath("assets/vm_config_io.json")
                 .setDebugLevel(DEBUG_LEVEL_FULL)
                 .build();
         List<Double> readRates = new ArrayList<>(IO_TEST_TRIAL_COUNT);
@@ -282,11 +288,11 @@
     @Test
     public void testMemoryUsage() throws Exception {
         final String vmName = "test_vm_mem_usage";
-        VirtualMachineConfig config =
-                mInner.newVmConfigBuilder("assets/vm_config_io.json")
-                        .setDebugLevel(DEBUG_LEVEL_NONE)
-                        .setMemoryMib(256)
-                        .build();
+        VirtualMachineConfig config = mInner.newVmConfigBuilder()
+                .setPayloadConfigPath("assets/vm_config_io.json")
+                .setDebugLevel(DEBUG_LEVEL_NONE)
+                .setMemoryMib(256)
+                .build();
         mInner.forceCreateNewVirtualMachine(vmName, config);
         VirtualMachine vm = mInner.getVirtualMachineManager().get(vmName);
         MemoryUsageListener listener = new MemoryUsageListener(this::executeCommand);
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 07705ba..66cd211 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
@@ -19,6 +19,7 @@
 
 import static org.junit.Assume.assumeNoException;
 
+import android.app.Instrumentation;
 import android.app.UiAutomation;
 import android.content.Context;
 import android.os.ParcelFileDescriptor;
@@ -32,6 +33,7 @@
 
 import androidx.annotation.CallSuper;
 import androidx.test.core.app.ApplicationProvider;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.microdroid.test.common.MetricsProcessor;
 import com.android.virt.VirtualizationTestHelper;
@@ -58,6 +60,20 @@
                 SystemProperties.get("debug.hypervisor.metrics_tag"));
     }
 
+    protected final void grantPermission(String permission) {
+        Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+        UiAutomation uiAutomation = instrumentation.getUiAutomation();
+        uiAutomation.grantRuntimePermission(instrumentation.getContext().getPackageName(),
+                permission);
+    }
+
+    protected final void revokePermission(String permission) {
+        Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+        UiAutomation uiAutomation = instrumentation.getUiAutomation();
+        uiAutomation.revokeRuntimePermission(instrumentation.getContext().getPackageName(),
+                permission);
+    }
+
     // TODO(b/220920264): remove Inner class; this is a hack to hide virt APEX types
     protected static class Inner {
         private final boolean mProtectedVm;
@@ -82,10 +98,6 @@
             return new VirtualMachineConfig.Builder(mContext).setProtectedVm(mProtectedVm);
         }
 
-        public VirtualMachineConfig.Builder newVmConfigBuilder(String payloadConfigPath) {
-            return newVmConfigBuilder().setPayloadConfigPath(payloadConfigPath);
-        }
-
         /**
          * Creates a new virtual machine, potentially removing an existing virtual machine with
          * given name.
diff --git a/tests/hostside/AndroidTest.xml b/tests/hostside/AndroidTest.xml
index 5c3e5d1..18728ad 100644
--- a/tests/hostside/AndroidTest.xml
+++ b/tests/hostside/AndroidTest.xml
@@ -13,7 +13,7 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<configuration description="Tests for microdroid">
+<configuration description="Host driven tests for Microdroid">
     <option name="test-suite-tag" value="cts" />
     <option name="config-descriptor:metadata" key="component" value="security" />
     <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
diff --git a/tests/hostside/java/com/android/microdroid/test/MicrodroidTestCase.java b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
similarity index 94%
rename from tests/hostside/java/com/android/microdroid/test/MicrodroidTestCase.java
rename to tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
index 190c524..c9df624 100644
--- a/tests/hostside/java/com/android/microdroid/test/MicrodroidTestCase.java
+++ b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
@@ -38,11 +38,8 @@
 import com.android.os.AtomsProto;
 import com.android.os.StatsLog;
 import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.result.TestDescription;
-import com.android.tradefed.result.TestResult;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestMetrics;
-import com.android.tradefed.testtype.junit4.DeviceTestRunOptions;
 import com.android.tradefed.util.CommandResult;
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.RunUtil;
@@ -73,7 +70,7 @@
 import java.util.regex.Pattern;
 
 @RunWith(DeviceJUnit4ClassRunner.class)
-public class MicrodroidTestCase extends MicrodroidHostTestCaseBase {
+public class MicrodroidHostTests extends MicrodroidHostTestCaseBase {
     private static final String APK_NAME = "MicrodroidTestApp.apk";
     private static final String PACKAGE_NAME = "com.android.microdroid.test";
     private static final String SHELL_PACKAGE_NAME = "com.android.shell";
@@ -106,29 +103,6 @@
         runOnMicrodroidForResult("watch -e \"getprop dev.bootcomplete | grep '^0$'\"");
     }
 
-    @Test
-    @CddTest(requirements = {"9.17/C-1-1", "9.17/C-1-2", "9.17/C-1-4"})
-    public void testCreateVmRequiresPermission() throws Exception {
-        // Revoke the MANAGE_VIRTUAL_MACHINE permission for the test app
-        CommandRunner android = new CommandRunner(getDevice());
-        android.run("pm", "revoke", PACKAGE_NAME, "android.permission.MANAGE_VIRTUAL_MACHINE");
-
-        // Run MicrodroidTests#connectToVmService test, which should fail
-        final DeviceTestRunOptions options =
-                new DeviceTestRunOptions(PACKAGE_NAME)
-                        .setTestClassName(PACKAGE_NAME + ".MicrodroidTests")
-                        .setTestMethodName("connectToVmService[protectedVm=false]")
-                        .setCheckResults(false);
-        assertThat(runDeviceTests(options)).isFalse();
-
-        Map<TestDescription, TestResult> results = getLastDeviceRunResults().getTestResults();
-        assertThat(results).hasSize(1);
-        TestResult result = results.values().toArray(new TestResult[0])[0];
-        assertWithMessage("The test should fail with a permission error")
-                .that(result.getStackTrace())
-                .contains("android.permission.MANAGE_VIRTUAL_MACHINE permission");
-    }
-
     private static JSONObject newPartition(String label, String path) {
         return new JSONObject(Map.of("label", label, "path", path));
     }
@@ -364,13 +338,10 @@
         final String initrdPath = TEST_ROOT + "etc/microdroid_initrd_full_debuggable.img";
         config.put("initrd", initrdPath);
         // Add instance image as a partition in disks[1]
-        disks.put(
-            new JSONObject()
+        disks.put(new JSONObject()
                 .put("writable", true)
-                .put(
-                    "partitions",
-                    new JSONArray()
-                        .put(newPartition("vm-instance", instanceImgPath))));
+                .put("partitions",
+                        new JSONArray().put(newPartition("vm-instance", instanceImgPath))));
         // Add payload image disk with partitions:
         // - payload-metadata
         // - apexes: com.android.os.statsd, com.android.adbd, [sharedlib apex](optional)
diff --git a/tests/testapk/AndroidManifest.xml b/tests/testapk/AndroidManifest.xml
index 9c8b2d5..ab22546 100644
--- a/tests/testapk/AndroidManifest.xml
+++ b/tests/testapk/AndroidManifest.xml
@@ -16,6 +16,7 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
       package="com.android.microdroid.test">
     <uses-permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE" />
+    <uses-permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE" />
     <uses-sdk android:minSdkVersion="33" android:targetSdkVersion="33" />
     <application>
         <uses-library android:name="android.system.virtualmachine" android:required="false" />
diff --git a/tests/testapk/AndroidTest.xml b/tests/testapk/AndroidTest.xml
index e8bb1aa..787ebd4 100644
--- a/tests/testapk/AndroidTest.xml
+++ b/tests/testapk/AndroidTest.xml
@@ -13,7 +13,7 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<configuration description="Runs sample instrumentation test.">
+<configuration description="Runs Microdroid device-side tests.">
     <option name="test-suite-tag" value="cts" />
     <option name="config-descriptor:metadata" key="component" value="security" />
     <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
@@ -22,11 +22,6 @@
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="test-file-name" value="MicrodroidTestApp.apk" />
     </target_preparer>
-    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
-      <option
-        name="run-command"
-        value="pm grant com.android.microdroid.test android.permission.MANAGE_VIRTUAL_MACHINE" />
-    </target_preparer>
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="com.android.microdroid.test" />
         <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
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 466acc7..297341b 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -21,6 +21,8 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
 
+import static org.junit.Assert.assertThrows;
+
 import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
 
 import android.os.Build;
@@ -35,6 +37,7 @@
 import com.android.microdroid.test.device.MicrodroidDeviceTestBase;
 import com.android.microdroid.testservice.ITestService;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Ignore;
 import org.junit.Rule;
@@ -76,9 +79,16 @@
 
     @Before
     public void setup() {
+        grantPermission(VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION);
         prepareTestSetup(mProtectedVm);
     }
 
+    @After
+    public void tearDown() {
+        revokePermission(VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION);
+        revokePermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
+    }
+
     private static final int MIN_MEM_ARM64 = 150;
     private static final int MIN_MEM_X86_64 = 196;
 
@@ -106,12 +116,59 @@
     @Test
     @CddTest(requirements = {
             "9.17/C-1-1",
+            "9.17/C-1-2",
+            "9.17/C-1-4",
+    })
+    public void createVmRequiresPermission() throws Exception {
+        assumeSupportedKernel();
+
+        revokePermission(VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION);
+
+        VirtualMachineConfig config = mInner.newVmConfigBuilder()
+                .setPayloadBinaryPath("MicrodroidTestNativeLib.so")
+                .setMemoryMib(minMemoryRequired())
+                .build();
+
+        SecurityException e = assertThrows(SecurityException.class,
+                () -> mInner.forceCreateNewVirtualMachine("test_vm_requires_permission", config));
+        assertThat(e).hasMessageThat()
+                .contains("android.permission.MANAGE_VIRTUAL_MACHINE permission");
+    }
+
+    @Test
+    @CddTest(requirements = {
+            "9.17/C-1-1",
+            "9.17/C-1-2",
+            "9.17/C-1-4",
+    })
+    public void createVmWithConfigRequiresPermission() throws Exception {
+        assumeSupportedKernel();
+
+        VirtualMachineConfig config = mInner.newVmConfigBuilder()
+                .setPayloadConfigPath("assets/vm_config.json")
+                .setMemoryMib(minMemoryRequired())
+                .build();
+
+        VirtualMachine vm = mInner.forceCreateNewVirtualMachine(
+                "test_vm_config_requires_permission", config);
+
+        SecurityException e = assertThrows(SecurityException.class, () -> runVmTestService(vm));
+        assertThat(e).hasMessageThat()
+                .contains("android.permission.USE_CUSTOM_VIRTUAL_MACHINE permission");
+    }
+
+
+    @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")
+        grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
+        VirtualMachineConfig config = mInner.newVmConfigBuilder()
+                .setPayloadConfigPath("assets/vm_config_extra_apk.json")
                 .setMemoryMib(minMemoryRequired())
                 .build();
         VirtualMachine vm = mInner.forceCreateNewVirtualMachine("test_vm_extra_apk", config);
@@ -406,9 +463,11 @@
 
     @Test
     public void bootFailsWhenConfigIsInvalid() throws Exception {
-        VirtualMachineConfig.Builder builder =
-                mInner.newVmConfigBuilder("assets/vm_config_no_task.json");
-        VirtualMachineConfig normalConfig = builder.setDebugLevel(DEBUG_LEVEL_FULL).build();
+        grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
+        VirtualMachineConfig normalConfig = mInner.newVmConfigBuilder()
+                .setPayloadConfigPath("assets/vm_config_no_task.json")
+                .setDebugLevel(DEBUG_LEVEL_FULL)
+                .build();
         mInner.forceCreateNewVirtualMachine("test_vm_invalid_config", normalConfig);
 
         BootResult bootResult = tryBootVm(TAG, "test_vm_invalid_config");
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index 1eec765..de63e3d 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -340,10 +340,18 @@
     ) -> binder::Result<Strong<dyn IVirtualMachine>> {
         check_manage_access()?;
 
-        if let VirtualMachineConfig::RawConfig(config) = config {
-            if config.protectedVm {
-                check_use_custom_virtual_machine()?;
+        let is_custom = match config {
+            VirtualMachineConfig::RawConfig(_) => true,
+            VirtualMachineConfig::AppConfig(config) => {
+                // Some features are reserved for platform apps only, even when using
+                // VirtualMachineAppConfig:
+                // - controlling CPUs;
+                // - specifying a config file in the APK.
+                !config.taskProfiles.is_empty() || matches!(config.payload, Payload::ConfigPath(_))
             }
+        };
+        if is_custom {
+            check_use_custom_virtual_machine()?;
         }
 
         let state = &mut *self.state.lock().unwrap();
@@ -377,18 +385,17 @@
             )
         })?;
 
-        let is_app_config = matches!(config, VirtualMachineConfig::AppConfig(_));
-
-        let config = match config {
-            VirtualMachineConfig::AppConfig(config) => BorrowedOrOwned::Owned(
-                load_app_config(config, &temporary_directory).map_err(|e| {
+        let (is_app_config, config) = match config {
+            VirtualMachineConfig::RawConfig(config) => (false, BorrowedOrOwned::Borrowed(config)),
+            VirtualMachineConfig::AppConfig(config) => {
+                let config = load_app_config(config, &temporary_directory).map_err(|e| {
                     *is_protected = config.protectedVm;
                     let message = format!("Failed to load app config: {:?}", e);
                     error!("{}", message);
                     Status::new_service_specific_error_str(-1, Some(message))
-                })?,
-            ),
-            VirtualMachineConfig::RawConfig(config) => BorrowedOrOwned::Borrowed(config),
+                })?;
+                (true, BorrowedOrOwned::Owned(config))
+            }
         };
         let config = config.as_ref();
         *is_protected = config.protectedVm;
@@ -590,12 +597,6 @@
     config: &VirtualMachineAppConfig,
     temporary_directory: &Path,
 ) -> Result<VirtualMachineRawConfig> {
-    // Controlling CPUs is reserved for platform apps only, even when using
-    // VirtualMachineAppConfig.
-    if !config.taskProfiles.is_empty() {
-        check_use_custom_virtual_machine()?
-    }
-
     let apk_file = clone_file(config.apk.as_ref().unwrap())?;
     let idsig_file = clone_file(config.idsig.as_ref().unwrap())?;
     let instance_file = clone_file(config.instanceImage.as_ref().unwrap())?;