Virtualizationservice makes payload disk image

with given APK, idsig, and config file.

To exercise the new execution mode, 'vm run-app' sub command is added.

  $ vm <apk_path> <idsig_path> <config_path>

For example,

  $ vm /data/local/tmp/MyApp.apk /data/local/tmp/MyApp.apk.idsig \
    assets/config.json

Bug: 190503456
Test: MicrodroidHostTestCases, VirtualizationTestCases
Change-Id: Iceec9b34e9785a1ae36452bfc2653c3c045f4dfa
diff --git a/microdroid/README.md b/microdroid/README.md
index b9d2086..2d5f854 100644
--- a/microdroid/README.md
+++ b/microdroid/README.md
@@ -130,63 +130,25 @@
 adb install out/dist/MyApp.apk
 ```
 
-### Creating `payload.img` manually (temporary step)
-
-This is a step that needs to be done manually for now. Eventually, this will be
-automatically done by a service named `virtualizationservice` which is part of
-the `com.android.virt` APEX.
-
-Create `payload.json` file:
-
-```json
-{
-  "payload_config_path": "/mnt/apk/assets/VM_CONFIG_NAME,
-  "system_apexes": [
-    "com.android.adbd",
-    "com.android.i18n",
-    "com.android.os.statsd",
-    "com.android.sdkext"
-  ],
-  "apk": {
-    "name": "PACKAGE_NAME_OF_YOUR_APP",
-    "path": "PATH_TO_YOUR_APP",
-    "idsig_path": "PATH_TO_APK_IDSIG"
-  }
-}
-```
-
-`ALL_CAP`s in the above are placeholders. They need to be replaced with correct
+`ALL_CAP`s below are placeholders. They need to be replaced with correct
 values:
-
 * `VM_CONFIG_FILE`: the name of the VM config file that you embedded in the APK.
-* `PACKAGE_NAME_OF_YOUR_APP`: package name of your app(e.g. `com.acme.app`).
+  (e.g. `vm_config.json`)
+* `PACKAGE_NAME_OF_YOUR_APP`: package name of your app (e.g. `com.acme.app`).
 * `PATH_TO_YOUR_APP`: path to the installed APK on the device. Can be obtained
   via the following command.
+  ```sh
+  adb shell pm path PACKAGE_NAME_OF_YOUR_APP
+  ```
+  It shall report a cryptic path similar to `/data/app/~~OgZq==/com.acme.app-HudMahQ==/base.apk`.
 
-```sh
-adb shell pm path PACKAGE_NAME_OF_YOUR_APP
-```
-
-It shall report a cryptic path similar to
-`/data/app/~~OgZq==/com.acme.app-HudMahQ==/base.apk`.
-
-* `PATH_TO_APK_IDSIG`: path to the pushed APK idsig on the device. See below
-  `adb push` command: it will be `/data/local/tmp/virt/MyApp.apk.idsig` in this
-  example.
-
-Once the file is done, execute the following command to push it to the device
-and run `mk_payload` to create `payload.img`:
+Push idsig of the APK to the device.
 
 ```sh
 TEST_ROOT=/data/local/tmp/virt
-adb push out/dist/MyApp.apk.idsig $TEST_ROOT/MyApp.apk.idsig
-adb push path_to_payload.json $TEST_ROOT/payload.json
-adb shell /apex/com.android.virt/bin/mk_payload $TEST_ROOT/payload.json $TEST_ROOT/payload.img
-adb shell chmod go+r $TEST_ROOT/payload*
+adb push out/dist/MyApp.apk.idsig $TEST_ROOT
 ```
 
-### Running the VM
-
 Execute the following commands to launch a VM. The VM will boot to microdroid
 and then automatically execute your app (the shared library
 `MyMicrodroidApp.so`).
@@ -196,7 +158,7 @@
 adb root
 adb shell setenforce 0
 adb shell start virtualizationservice
-adb shell /apex/com.android.virt/bin/vm run --daemonize --log $TEST_ROOT/log.txt /apex/com.android.virt/etc/microdroid.json
+adb shell /apex/com.android.virt/bin/vm run-app --daemonize --log $TEST_ROOT/log.txt PATH_TO_YOUR_APP $TEST_ROOT/MyApp.apk.idsig assets/VM_CONFIG_FILE
 ```
 
 The last command lets you know the CID assigned to the VM. The console output
diff --git a/microdroid/microdroid.json b/microdroid/microdroid.json
index 7dc4b6a..fbbee29 100644
--- a/microdroid/microdroid.json
+++ b/microdroid/microdroid.json
@@ -34,10 +34,6 @@
         }
       ],
       "writable": false
-    },
-    {
-      "image": "/data/local/tmp/virt/payload.img",
-      "writable": false
     }
   ]
 }
diff --git a/microdroid/payload/Android.bp b/microdroid/payload/Android.bp
index 5ea6c10..c7bc415 100644
--- a/microdroid/payload/Android.bp
+++ b/microdroid/payload/Android.bp
@@ -44,6 +44,9 @@
     protos: ["metadata.proto"],
     source_stem: "microdroid_metadata",
     host_supported: true,
+    apex_available: [
+        "com.android.virt",
+    ],
 }
 
 cc_binary {
diff --git a/microdroid/payload/config/Android.bp b/microdroid/payload/config/Android.bp
index da58bdf..827f6e3 100644
--- a/microdroid/payload/config/Android.bp
+++ b/microdroid/payload/config/Android.bp
@@ -13,4 +13,7 @@
         "libserde_json",
         "libserde",
     ],
+    apex_available: [
+        "com.android.virt",
+    ],
 }
diff --git a/microdroid/payload/metadata/Android.bp b/microdroid/payload/metadata/Android.bp
index 4b23394..d3ec625 100644
--- a/microdroid/payload/metadata/Android.bp
+++ b/microdroid/payload/metadata/Android.bp
@@ -13,4 +13,7 @@
         "libmicrodroid_metadata_proto_rust",
         "libprotobuf",
     ],
+    apex_available: [
+        "com.android.virt",
+    ],
 }
diff --git a/tests/hostside/java/android/virt/test/MicrodroidTestCase.java b/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
index 87c8aee..a3288a1 100644
--- a/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
+++ b/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
@@ -18,7 +18,6 @@
 
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.CoreMatchers.not;
 import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
@@ -31,27 +30,17 @@
 import com.android.tradefed.util.CommandStatus;
 import com.android.tradefed.util.RunUtil;
 
-import org.json.JSONArray;
-import org.json.JSONObject;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import java.io.BufferedReader;
 import java.io.File;
-import java.io.FileWriter;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.List;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
-import java.util.stream.Collectors;
-import java.util.zip.ZipFile;
 
 @RunWith(DeviceJUnit4ClassRunner.class)
 public class MicrodroidTestCase extends BaseHostJUnit4Test {
@@ -178,26 +167,16 @@
         return String.join(" ", Arrays.asList(strs));
     }
 
-    private String createPayloadImage(String apkName, String packageName, String configPath)
+    private File findTestFile(String name) throws Exception {
+        return (new CompatibilityBuildHelper(getBuild())).getTestFile(name);
+    }
+
+    private String startMicrodroid(String apkName, String packageName, String configPath)
             throws Exception {
+        // Install APK
         File apkFile = findTestFile(apkName);
         getDevice().installPackage(apkFile, /* reinstall */ true);
 
-        // Read the config file from the apk and parse it to know the list of APEXes needed
-        ZipFile apkAsZip = new ZipFile(apkFile);
-        InputStream is = apkAsZip.getInputStream(apkAsZip.getEntry(configPath));
-        String configString =
-                new BufferedReader(new InputStreamReader(is))
-                        .lines()
-                        .collect(Collectors.joining("\n"));
-        JSONObject configObject = new JSONObject(configString);
-        JSONArray apexes = configObject.getJSONArray("apexes");
-        List<String> apexNames = new ArrayList<>();
-        for (int i = 0; i < apexes.length(); i++) {
-            JSONObject anApex = apexes.getJSONObject(i);
-            apexNames.add(anApex.getString("name"));
-        }
-
         // Get the path to the installed apk. Note that
         // getDevice().getAppPackageInfo(...).getCodePath() doesn't work due to the incorrect
         // parsing of the "=" character. (b/190975227). So we use the `pm path` command directly.
@@ -210,45 +189,6 @@
         final String apkIdsigPath = TEST_ROOT + apkName + ".idsig";
         getDevice().pushFile(idsigOnHost, apkIdsigPath);
 
-        // Create payload.json from the gathered data
-        JSONObject payloadObject = new JSONObject();
-        payloadObject.put("system_apexes", new JSONArray(apexNames));
-        payloadObject.put("payload_config_path", "/mnt/apk/" + configPath);
-        JSONObject apkObject = new JSONObject();
-        apkObject.put("name", packageName);
-        apkObject.put("path", apkPath);
-        apkObject.put("idsig_path", apkIdsigPath);
-        payloadObject.put("apk", apkObject);
-
-        // Copy the json file to Android
-        File payloadJsonOnHost = File.createTempFile("payload", "json");
-        FileWriter writer = new FileWriter(payloadJsonOnHost);
-        writer.write(payloadObject.toString());
-        writer.close();
-        final String payloadJson = TEST_ROOT + "payload.json";
-        getDevice().pushFile(payloadJsonOnHost, payloadJson);
-
-        // Finally run mk_payload to create payload.img
-        final String mkPayload = VIRT_APEX + "bin/mk_payload";
-        final String payloadImg = TEST_ROOT + "payload.img";
-        runOnAndroid(mkPayload, payloadJson, payloadImg);
-        assertThat(runOnAndroid("du", "-b", payloadImg), is(not("")));
-
-        // The generated files are owned by root. Allow the virtualizationservice to read them.
-        runOnAndroid("chmod", "go+r", TEST_ROOT + "payload*");
-
-        return payloadImg;
-    }
-
-    private File findTestFile(String name) throws Exception {
-        return (new CompatibilityBuildHelper(getBuild())).getTestFile(name);
-    }
-
-    private String startMicrodroid(String apkName, String packageName, String configPath)
-            throws Exception {
-        // Create payload.img
-        createPayloadImage(apkName, packageName, configPath);
-
         final String logPath = TEST_ROOT + "log.txt";
 
         // Run the VM
@@ -256,10 +196,12 @@
         String ret =
                 runOnAndroid(
                         VIRT_APEX + "bin/vm",
-                        "run",
+                        "run-app",
                         "--daemonize",
                         "--log " + logPath,
-                        VIRT_APEX + "etc/microdroid.json");
+                        apkPath,
+                        apkIdsigPath,
+                        configPath);
 
         // Redirect log.txt to logd using logwrapper
         ExecutorService executor = Executors.newFixedThreadPool(1);
diff --git a/tests/vsock_test.cc b/tests/vsock_test.cc
index 931e79d..d9b8f21 100644
--- a/tests/vsock_test.cc
+++ b/tests/vsock_test.cc
@@ -32,6 +32,7 @@
 #include "android-base/parseint.h"
 #include "android-base/unique_fd.h"
 #include "android/system/virtualizationservice/VirtualMachineConfig.h"
+#include "android/system/virtualizationservice/VirtualMachineRawConfig.h"
 #include "virt/VirtualizationTest.h"
 
 #define KVM_CAP_ARM_PROTECTED_VM 0xffbadab1
@@ -83,12 +84,13 @@
     ret = TEMP_FAILURE_RETRY(listen(server_fd, 1));
     ASSERT_EQ(ret, 0) << strerror(errno);
 
-    VirtualMachineConfig config;
-    config.kernel = ParcelFileDescriptor(unique_fd(open(kVmKernelPath, O_RDONLY | O_CLOEXEC)));
-    config.initrd = ParcelFileDescriptor(unique_fd(open(kVmInitrdPath, O_RDONLY | O_CLOEXEC)));
-    config.params = kVmParams;
-    config.protected_vm = protected_vm;
+    VirtualMachineRawConfig raw_config;
+    raw_config.kernel = ParcelFileDescriptor(unique_fd(open(kVmKernelPath, O_RDONLY | O_CLOEXEC)));
+    raw_config.initrd = ParcelFileDescriptor(unique_fd(open(kVmInitrdPath, O_RDONLY | O_CLOEXEC)));
+    raw_config.params = kVmParams;
+    raw_config.protected_vm = protected_vm;
 
+    VirtualMachineConfig config(std::move(raw_config));
     sp<IVirtualMachine> vm;
     status = virtualization_service->startVm(config, std::nullopt, &vm);
     ASSERT_TRUE(status.isOk()) << "Error starting VM: " << status;
diff --git a/virtualizationservice/Android.bp b/virtualizationservice/Android.bp
index 700d0fc..a941742 100644
--- a/virtualizationservice/Android.bp
+++ b/virtualizationservice/Android.bp
@@ -27,12 +27,16 @@
         "libcrc32fast",
         "libdisk",
         "liblog_rust",
+        "libmicrodroid_metadata",
+        "libmicrodroid_payload_config",
         "libprotobuf",
         "libprotos",
         "libserde_json",
         "libserde",
         "libshared_child",
         "libuuid",
+        "libvmconfig",
+        "libzip",
     ],
 }
 
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
new file mode 100644
index 0000000..5b270a3
--- /dev/null
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2021 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 android.system.virtualizationservice;
+
+/** Configuration for running an App in a VM */
+parcelable VirtualMachineAppConfig {
+    /** Main APK */
+    ParcelFileDescriptor apk;
+
+    /** idsig for an APK */
+    ParcelFileDescriptor idsig;
+
+    /** Path to a configuration in an APK. This is the actual configuration for a VM. */
+    @utf8InCpp String configPath;
+}
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineConfig.aidl
index 5d59f9d..00a7937 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineConfig.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineConfig.aidl
@@ -15,31 +15,14 @@
  */
 package android.system.virtualizationservice;
 
-import android.system.virtualizationservice.DiskImage;
+import android.system.virtualizationservice.VirtualMachineAppConfig;
+import android.system.virtualizationservice.VirtualMachineRawConfig;
 
-/** Configuration for running a VM. */
-parcelable VirtualMachineConfig {
-    /** The kernel image, if any. */
-    @nullable ParcelFileDescriptor kernel;
+/** Configuration for running a VM */
+union VirtualMachineConfig {
+    /** Configuration for a VM to run an APP */
+    VirtualMachineAppConfig appConfig;
 
-    /** The initial ramdisk for the kernel, if any. */
-    @nullable ParcelFileDescriptor initrd;
-
-    /**
-     * Parameters to pass to the kernel. As far as the VMM and boot protocol are concerned this is
-     * just a string, but typically it will contain multiple parameters separated by spaces.
-     */
-    @nullable @utf8InCpp String params;
-
-    /**
-     * The bootloader to use. If this is supplied then the kernel and initrd must not be supplied;
-     * the bootloader is instead responsibly for loading the kernel from one of the disks.
-     */
-    @nullable ParcelFileDescriptor bootloader;
-
-    /** Disk images to be made available to the VM. */
-    DiskImage[] disks;
-
-    /** Whether the VM should be a protected VM. */
-    boolean protected_vm;
+    /** Configuration for a VM with low-level configuration */
+    VirtualMachineRawConfig rawConfig;
 }
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
new file mode 100644
index 0000000..7848ed5
--- /dev/null
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2021 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 android.system.virtualizationservice;
+
+import android.system.virtualizationservice.DiskImage;
+
+/** Raw Configuration for running a VM. */
+parcelable VirtualMachineRawConfig {
+    /** The kernel image, if any. */
+    @nullable ParcelFileDescriptor kernel;
+
+    /** The initial ramdisk for the kernel, if any. */
+    @nullable ParcelFileDescriptor initrd;
+
+    /**
+     * Parameters to pass to the kernel. As far as the VMM and boot protocol are concerned this is
+     * just a string, but typically it will contain multiple parameters separated by spaces.
+     */
+    @nullable @utf8InCpp String params;
+
+    /**
+     * The bootloader to use. If this is supplied then the kernel and initrd must not be supplied;
+     * the bootloader is instead responsibly for loading the kernel from one of the disks.
+     */
+    @nullable ParcelFileDescriptor bootloader;
+
+    /** Disk images to be made available to the VM. */
+    DiskImage[] disks;
+
+    /** Whether the VM should be a protected VM. */
+    boolean protected_vm;
+}
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index ce9a080..3f1660e 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -16,26 +16,35 @@
 
 use crate::composite::make_composite_image;
 use crate::crosvm::{CrosvmConfig, DiskFile, VmInstance};
+use crate::payload;
 use crate::{Cid, FIRST_GUEST_CID};
+
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::IVirtualizationService::IVirtualizationService;
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::DiskImage::DiskImage;
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::IVirtualMachine::{
     BnVirtualMachine, IVirtualMachine,
 };
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::IVirtualMachineCallback::IVirtualMachineCallback;
-use android_system_virtualizationservice::aidl::android::system::virtualizationservice::VirtualMachineConfig::VirtualMachineConfig;
+use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
+    VirtualMachineAppConfig::VirtualMachineAppConfig,
+    VirtualMachineConfig::VirtualMachineConfig,
+    VirtualMachineRawConfig::VirtualMachineRawConfig,
+};
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::VirtualMachineDebugInfo::VirtualMachineDebugInfo;
 use android_system_virtualizationservice::binder::{
     self, BinderFeatures, ExceptionCode, Interface, ParcelFileDescriptor, Status, Strong, ThreadState,
 };
+use anyhow::Error;
 use disk::QcowFile;
 use log::{debug, error, warn};
+use microdroid_payload_config::VmPayloadConfig;
 use std::convert::TryInto;
 use std::ffi::CString;
 use std::fs::{File, create_dir};
 use std::os::unix::io::AsRawFd;
 use std::path::{Path, PathBuf};
 use std::sync::{Arc, Mutex, Weak};
+use vmconfig::VmConfig;
 
 pub const BINDER_SERVICE_IDENTIFIER: &str = "android.system.virtualizationservice";
 
@@ -92,6 +101,22 @@
             )
         })?;
 
+        let mut opt_raw_config = None;
+        let config = match config {
+            VirtualMachineConfig::AppConfig(config) => {
+                let raw_config = load_app_config(config, &temporary_directory).map_err(|e| {
+                    error!("Failed to load app config from {}: {}", &config.configPath, e);
+                    new_binder_exception(
+                        ExceptionCode::SERVICE_SPECIFIC,
+                        format!("Failed to load app config from {}: {}", &config.configPath, e),
+                    )
+                })?;
+                opt_raw_config.replace(raw_config);
+                opt_raw_config.as_ref().unwrap()
+            }
+            VirtualMachineConfig::RawConfig(config) => config,
+        };
+
         // Assemble disk images if needed.
         let disks = config
             .disks
@@ -256,6 +281,34 @@
     Ok(DiskFile { image, writable: disk.writable })
 }
 
+fn load_app_config(
+    config: &VirtualMachineAppConfig,
+    temporary_directory: &Path,
+) -> Result<VirtualMachineRawConfig, Error> {
+    let apk_file = config.apk.as_ref().unwrap().as_ref();
+    let idsig_file = config.idsig.as_ref().unwrap().as_ref();
+    let config_path = &config.configPath;
+
+    let mut apk_zip = zip::ZipArchive::new(apk_file)?;
+    let config_file = apk_zip.by_name(config_path)?;
+    let vm_payload_config: VmPayloadConfig = serde_json::from_reader(config_file)?;
+
+    let os_name = &vm_payload_config.os.name;
+    let vm_config_path = PathBuf::from(format!("/apex/com.android.virt/etc/{}.json", os_name));
+    let vm_config_file = File::open(vm_config_path)?;
+    let mut vm_config = VmConfig::load(&vm_config_file)?;
+
+    vm_config.disks.push(payload::make_disk_image(
+        format!("/proc/self/fd/{}", apk_file.as_raw_fd()).into(),
+        format!("/proc/self/fd/{}", idsig_file.as_raw_fd()).into(),
+        config_path,
+        &vm_payload_config.apexes,
+        temporary_directory,
+    )?);
+
+    vm_config.to_parcelable()
+}
+
 /// Generates a unique filename to use for a composite disk image.
 fn make_composite_image_filenames(
     temporary_directory: &Path,
diff --git a/virtualizationservice/src/composite.rs b/virtualizationservice/src/composite.rs
index 7b5a258..1af0eed 100644
--- a/virtualizationservice/src/composite.rs
+++ b/virtualizationservice/src/composite.rs
@@ -72,6 +72,11 @@
     ((val + (align - 1)) / align) * align
 }
 
+/// Round `val` to partition size(4K)
+pub fn align_to_partition_size(val: u64) -> u64 {
+    align_to_power_of_2(val, PARTITION_SIZE_SHIFT)
+}
+
 impl PartitionInfo {
     fn aligned_size(&self) -> u64 {
         align_to_power_of_2(self.files.iter().map(|file| file.size).sum(), PARTITION_SIZE_SHIFT)
diff --git a/virtualizationservice/src/main.rs b/virtualizationservice/src/main.rs
index 43b5fe4..658203b 100644
--- a/virtualizationservice/src/main.rs
+++ b/virtualizationservice/src/main.rs
@@ -18,6 +18,7 @@
 mod composite;
 mod crosvm;
 mod gpt;
+mod payload;
 
 use crate::aidl::{VirtualizationService, BINDER_SERVICE_IDENTIFIER};
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::IVirtualizationService::BnVirtualizationService;
diff --git a/virtualizationservice/src/payload.rs b/virtualizationservice/src/payload.rs
new file mode 100644
index 0000000..19a6d9f
--- /dev/null
+++ b/virtualizationservice/src/payload.rs
@@ -0,0 +1,140 @@
+// Copyright 2021, 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.
+
+//! Payload disk image
+
+use crate::composite::align_to_partition_size;
+
+use anyhow::{Error, Result};
+use microdroid_metadata::{ApexPayload, ApkPayload, Metadata};
+use microdroid_payload_config::ApexConfig;
+use std::fs;
+use std::fs::OpenOptions;
+use std::io::{Seek, SeekFrom, Write};
+use std::path::{Path, PathBuf};
+use std::process::Command;
+use vmconfig::{DiskImage, Partition};
+
+// TODO(b/191601801): look up /apex/apex-info-list.xml
+fn get_path(package_name: &str) -> Result<PathBuf> {
+    let output = Command::new("pm").arg("path").arg(package_name).output()?;
+    let output = String::from_utf8(output.stdout)?;
+    Ok(PathBuf::from(output.strip_prefix("package:").unwrap().trim()))
+}
+
+/// When passing a host APEX file as a block device in a payload disk image,
+/// the size of the original file needs to be stored in the last 4 bytes so that
+/// other programs (e.g. apexd) can read it as a zip.
+fn make_size_filler(size: u64, filler_path: &Path) -> Result<bool> {
+    let partition_size = align_to_partition_size(size + 4);
+    let mut file = OpenOptions::new().create_new(true).write(true).open(filler_path)?;
+    file.set_len(partition_size - size)?;
+    file.seek(SeekFrom::End(-4))?;
+    file.write_all(&(size as i32).to_be_bytes())?;
+    Ok(true)
+}
+
+/// When passing a host APK file as a block device in a payload disk image and it is
+/// mounted via dm-verity, we need to make the device zero-padded up to 4K boundary.
+/// Otherwise, intergrity checks via hashtree will fail.
+fn make_zero_filler(size: u64, filler_path: &Path) -> Result<bool> {
+    let partition_size = align_to_partition_size(size);
+    if partition_size <= size {
+        return Ok(false);
+    }
+    let file = OpenOptions::new().create_new(true).write(true).open(filler_path)?;
+    file.set_len(partition_size - size)?;
+    Ok(true)
+}
+
+/// When passing a host idsig file as a block device, we don't need any filler because it is read
+/// in length-prefixed way.
+fn make_no_filler(_size: u64, _filler_path: &Path) -> Result<bool> {
+    Ok(false)
+}
+
+/// Creates a DiskImage with partitions:
+///   metadata: metadata
+///   microdroid-apex-0: [apex 0, size filler]
+///   microdroid-apex-1: [apex 1, size filler]
+///   ..
+///   microdroid-apk: [apk, zero filler]
+///   microdroid-apk-idsig: idsig
+pub fn make_disk_image(
+    apk_file: PathBuf,
+    idsig_file: PathBuf,
+    config_path: &str,
+    apexes: &[ApexConfig],
+    temporary_directory: &Path,
+) -> Result<DiskImage> {
+    let metadata_path = temporary_directory.join("metadata");
+    let metadata = Metadata {
+        version: 1u32,
+        apexes: apexes
+            .iter()
+            .map(|apex| ApexPayload { name: String::from(&apex.name), ..Default::default() })
+            .collect(),
+        apk: Some(ApkPayload {
+            name: String::from("apk"),
+            payload_partition_name: String::from("microdroid-apk"),
+            idsig_partition_name: String::from("microdroid-apk-idsig"),
+            ..Default::default()
+        })
+        .into(),
+        payload_config_path: format!("/mnt/apk/{}", config_path),
+        ..Default::default()
+    };
+    let mut metadata_file =
+        OpenOptions::new().create_new(true).read(true).write(true).open(&metadata_path)?;
+    microdroid_metadata::write_metadata(&metadata, &mut metadata_file)?;
+
+    // put metadata at the first partition
+    let mut partitions = vec![Partition {
+        label: String::from("metadata"),
+        path: Some(metadata_path),
+        paths: vec![],
+        writable: false,
+    }];
+
+    let mut filler_count = 0;
+    let mut make_partition = |label: String,
+                              path: PathBuf,
+                              make_filler: &dyn Fn(u64, &Path) -> Result<bool, Error>|
+     -> Result<Partition> {
+        let filler_path = temporary_directory.join(format!("filler-{}", filler_count));
+        let size = fs::metadata(&path)?.len();
+
+        if make_filler(size, &filler_path)? {
+            filler_count += 1;
+            Ok(Partition { label, path: None, paths: vec![path, filler_path], writable: false })
+        } else {
+            Ok(Partition { label, path: Some(path), paths: vec![], writable: false })
+        }
+    };
+    for (i, apex) in apexes.iter().enumerate() {
+        partitions.push(make_partition(
+            format!("microdroid-apex-{}", i),
+            get_path(&apex.name)?,
+            &make_size_filler,
+        )?);
+    }
+    partitions.push(make_partition(String::from("microdroid-apk"), apk_file, &make_zero_filler)?);
+    partitions.push(make_partition(
+        String::from("microdroid-apk-idsig"),
+        idsig_file,
+        &make_no_filler,
+    )?);
+
+    Ok(DiskImage { image: None, partitions, writable: false })
+}
diff --git a/vm/src/main.rs b/vm/src/main.rs
index 8f16eb5..d7bae30 100644
--- a/vm/src/main.rs
+++ b/vm/src/main.rs
@@ -20,7 +20,7 @@
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::IVirtualizationService::IVirtualizationService;
 use android_system_virtualizationservice::binder::{wait_for_interface, ProcessState, Strong, ParcelFileDescriptor};
 use anyhow::{Context, Error};
-use run::command_run;
+use run::{command_run, command_run_app};
 use std::convert::TryInto;
 use std::fs::OpenOptions;
 use std::path::{PathBuf, Path};
@@ -33,6 +33,27 @@
 #[derive(StructOpt)]
 #[structopt(no_version, global_settings = &[AppSettings::DisableVersion])]
 enum Opt {
+    /// Run a virtual machine with a config in APK
+    RunApp {
+        /// Path to VM Payload APK
+        #[structopt(parse(from_os_str))]
+        apk: PathBuf,
+
+        /// Path to idsig of the APK
+        #[structopt(parse(from_os_str))]
+        idsig: PathBuf,
+
+        /// Path to VM config JSON within APK (e.g. assets/vm_config.json)
+        config_path: String,
+
+        /// Detach VM from the terminal and run in the background
+        #[structopt(short, long)]
+        daemonize: bool,
+
+        /// Path to file for VM log output.
+        #[structopt(short, long)]
+        log: Option<PathBuf>,
+    },
     /// Run a virtual machine
     Run {
         /// Path to VM config JSON
@@ -76,6 +97,9 @@
         .context("Failed to find VirtualizationService")?;
 
     match opt {
+        Opt::RunApp { apk, idsig, config_path, daemonize, log } => {
+            command_run_app(service, &apk, &idsig, &config_path, daemonize, log.as_deref())
+        }
         Opt::Run { config, daemonize, log } => {
             command_run(service, &config, daemonize, log.as_deref())
         }
diff --git a/vm/src/run.rs b/vm/src/run.rs
index fbf849b..1ae94ea 100644
--- a/vm/src/run.rs
+++ b/vm/src/run.rs
@@ -20,6 +20,10 @@
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::IVirtualMachineCallback::{
     BnVirtualMachineCallback, IVirtualMachineCallback,
 };
+use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
+    VirtualMachineAppConfig::VirtualMachineAppConfig,
+    VirtualMachineConfig::VirtualMachineConfig,
+};
 use android_system_virtualizationservice::binder::{
     BinderFeatures, DeathRecipient, IBinder, ParcelFileDescriptor, Strong,
 };
@@ -31,6 +35,25 @@
 use std::path::Path;
 use vmconfig::VmConfig;
 
+/// Run a VM from the given APK, idsig, and config.
+pub fn command_run_app(
+    service: Strong<dyn IVirtualizationService>,
+    apk: &Path,
+    idsig: &Path,
+    config_path: &str,
+    daemonize: bool,
+    log_path: Option<&Path>,
+) -> Result<(), Error> {
+    let apk_file = File::open(apk).context("Failed to open APK file")?;
+    let idsig_file = File::open(idsig).context("Failed to open idsig file")?;
+    let config = VirtualMachineConfig::AppConfig(VirtualMachineAppConfig {
+        apk: ParcelFileDescriptor::new(apk_file).into(),
+        idsig: ParcelFileDescriptor::new(idsig_file).into(),
+        configPath: config_path.to_owned(),
+    });
+    run(service, &config, &format!("{:?}!{:?}", apk, config_path), daemonize, log_path)
+}
+
 /// Run a VM from the given configuration file.
 pub fn command_run(
     service: Strong<dyn IVirtualizationService>,
@@ -41,6 +64,22 @@
     let config_file = File::open(config_path).context("Failed to open config file")?;
     let config =
         VmConfig::load(&config_file).context("Failed to parse config file")?.to_parcelable()?;
+    run(
+        service,
+        &VirtualMachineConfig::RawConfig(config),
+        &format!("{:?}", config_path),
+        daemonize,
+        log_path,
+    )
+}
+
+fn run(
+    service: Strong<dyn IVirtualizationService>,
+    config: &VirtualMachineConfig,
+    config_path: &str,
+    daemonize: bool,
+    log_path: Option<&Path>,
+) -> Result<(), Error> {
     let stdout = if let Some(log_path) = log_path {
         Some(ParcelFileDescriptor::new(
             File::create(log_path)
@@ -51,7 +90,7 @@
     } else {
         Some(ParcelFileDescriptor::new(duplicate_stdout()?))
     };
-    let vm = service.startVm(&config, stdout.as_ref()).context("Failed to start VM")?;
+    let vm = service.startVm(config, stdout.as_ref()).context("Failed to start VM")?;
 
     let cid = vm.getCid().context("Failed to get CID")?;
     println!("Started VM from {:?} with CID {}.", config_path, cid);
diff --git a/vmconfig/src/lib.rs b/vmconfig/src/lib.rs
index c9385f3..e5c8e1b 100644
--- a/vmconfig/src/lib.rs
+++ b/vmconfig/src/lib.rs
@@ -17,7 +17,7 @@
 use android_system_virtualizationservice::{
     aidl::android::system::virtualizationservice::DiskImage::DiskImage as AidlDiskImage,
     aidl::android::system::virtualizationservice::Partition::Partition as AidlPartition,
-    aidl::android::system::virtualizationservice::VirtualMachineConfig::VirtualMachineConfig,
+    aidl::android::system::virtualizationservice::VirtualMachineRawConfig::VirtualMachineRawConfig,
     binder::ParcelFileDescriptor,
 };
 
@@ -76,8 +76,8 @@
 
     /// Convert the `VmConfig` to a [`VirtualMachineConfig`] which can be passed to the Virt
     /// Manager.
-    pub fn to_parcelable(&self) -> Result<VirtualMachineConfig, Error> {
-        Ok(VirtualMachineConfig {
+    pub fn to_parcelable(&self) -> Result<VirtualMachineRawConfig, Error> {
+        Ok(VirtualMachineRawConfig {
             kernel: maybe_open_parcel_file(&self.kernel, false)?,
             initrd: maybe_open_parcel_file(&self.initrd, false)?,
             params: self.params.clone(),