Merge changes from topics "microdroid-no-bootstrap", "microdroid-starts-early"

* changes:
  Microdroid_manager starts before apexd
  microdroid runs in a single mount namespace
diff --git a/compos/compos_key_cmd/Android.bp b/compos/compos_key_cmd/Android.bp
index 1d889c6..e0584f4 100644
--- a/compos/compos_key_cmd/Android.bp
+++ b/compos/compos_key_cmd/Android.bp
@@ -12,6 +12,7 @@
     ],
 
     shared_libs: [
+        "android.system.virtualizationservice-ndk",
         "compos_aidl_interface-ndk",
         "libbase",
         "libbinder_rpc_unstable",
diff --git a/compos/compos_key_cmd/compos_key_cmd.cpp b/compos/compos_key_cmd/compos_key_cmd.cpp
index bee9de1..84a0a7c 100644
--- a/compos/compos_key_cmd/compos_key_cmd.cpp
+++ b/compos/compos_key_cmd/compos_key_cmd.cpp
@@ -14,12 +14,16 @@
  * limitations under the License.
  */
 
+#include <aidl/android/system/virtualizationservice/BnVirtualMachineCallback.h>
+#include <aidl/android/system/virtualizationservice/IVirtualizationService.h>
 #include <aidl/com/android/compos/ICompOsKeyService.h>
 #include <android-base/file.h>
+#include <android-base/logging.h>
 #include <android-base/result.h>
 #include <android-base/unique_fd.h>
 #include <android/binder_auto_utils.h>
 #include <android/binder_manager.h>
+#include <android/binder_process.h>
 #include <asm/byteorder.h>
 #include <libfsverity.h>
 #include <linux/fsverity.h>
@@ -27,9 +31,13 @@
 #include <openssl/mem.h>
 #include <openssl/sha.h>
 #include <openssl/x509.h>
+#include <unistd.h>
 
+#include <chrono>
+#include <condition_variable>
 #include <filesystem>
 #include <iostream>
+#include <mutex>
 #include <string>
 #include <string_view>
 
@@ -42,6 +50,11 @@
 
 using namespace std::literals;
 
+using aidl::android::system::virtualizationservice::BnVirtualMachineCallback;
+using aidl::android::system::virtualizationservice::IVirtualizationService;
+using aidl::android::system::virtualizationservice::IVirtualMachine;
+using aidl::android::system::virtualizationservice::IVirtualMachineCallback;
+using aidl::android::system::virtualizationservice::VirtualMachineConfig;
 using aidl::com::android::compos::CompOsKeyData;
 using aidl::com::android::compos::ICompOsKeyService;
 using android::base::ErrnoError;
@@ -49,8 +62,19 @@
 using android::base::Result;
 using android::base::unique_fd;
 using compos::proto::Signature;
+using ndk::ScopedAStatus;
+using ndk::ScopedFileDescriptor;
+using ndk::SharedRefBase;
 
-const unsigned int kRpcPort = 3142;
+constexpr unsigned int kRpcPort = 3142;
+
+constexpr const char* kConfigApkPath =
+        "/apex/com.android.compos/app/CompOSPayloadApp/CompOSPayloadApp.apk";
+constexpr const char* kConfigApkIdsigPath =
+        "/apex/com.android.compos/etc/CompOSPayloadApp.apk.idsig";
+
+// This is a path inside the APK
+constexpr const char* kConfigFilePath = "assets/key_service_vm_config.json";
 
 static bool writeBytesToFile(const std::vector<uint8_t>& bytes, const std::string& path) {
     std::string str(bytes.begin(), bytes.end());
@@ -66,11 +90,160 @@
 }
 
 static std::shared_ptr<ICompOsKeyService> getService(int cid) {
+    LOG(INFO) << "Connecting to cid " << cid;
     ndk::SpAIBinder binder(cid == 0 ? AServiceManager_getService("android.system.composkeyservice")
                                     : RpcClient(cid, kRpcPort));
     return ICompOsKeyService::fromBinder(binder);
 }
 
+namespace {
+class Callback : public BnVirtualMachineCallback {
+public:
+    ::ndk::ScopedAStatus onPayloadStarted(
+            int32_t in_cid, const ::ndk::ScopedFileDescriptor& /*in_stdout*/) override {
+        // TODO: Consider copying stdout somewhere useful?
+        LOG(INFO) << "Payload started! cid = " << in_cid;
+        {
+            std::unique_lock lock(mMutex);
+            mStarted = true;
+        }
+        mCv.notify_all();
+        return ScopedAStatus::ok();
+    }
+
+    ::ndk::ScopedAStatus onDied(int32_t in_cid) override {
+        LOG(WARNING) << "VM died! cid = " << in_cid;
+        {
+            std::unique_lock lock(mMutex);
+            mDied = true;
+        }
+        mCv.notify_all();
+        return ScopedAStatus::ok();
+    }
+
+    bool waitForStarted() {
+        std::unique_lock lock(mMutex);
+        return mCv.wait_for(lock, std::chrono::seconds(10), [this] { return mStarted || mDied; }) &&
+                !mDied;
+    }
+
+private:
+    std::mutex mMutex;
+    std::condition_variable mCv;
+    bool mStarted;
+    bool mDied;
+};
+
+class TargetVm {
+public:
+    TargetVm(int cid, const std::string& logFile, const std::string& instanceImageFile)
+          : mCid(cid), mLogFile(logFile), mInstanceImageFile(instanceImageFile) {}
+
+    // Returns 0 if we are to connect to a local service, otherwise the CID of
+    // either an existing VM or a VM we have started, depending on the command
+    // line arguments.
+    Result<int> resolveCid() {
+        if (mInstanceImageFile.empty()) {
+            return mCid;
+        }
+        if (mCid != 0) {
+            return Error() << "Can't specify both cid and image file.";
+        }
+
+        // We need a thread pool to receive VM callbacks.
+        ABinderProcess_startThreadPool();
+
+        ndk::SpAIBinder binder(
+                AServiceManager_waitForService("android.system.virtualizationservice"));
+        auto service = IVirtualizationService::fromBinder(binder);
+        if (!service) {
+            return Error() << "Failed to connect to virtualization service.";
+        }
+
+        ScopedFileDescriptor logFd;
+        if (mLogFile.empty()) {
+            logFd.set(dup(STDOUT_FILENO));
+            if (logFd.get() == -1) {
+                return ErrnoError() << "dup() failed: ";
+            }
+        } else {
+            logFd.set(TEMP_FAILURE_RETRY(open(mLogFile.c_str(),
+                                              O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC,
+                                              S_IRUSR | S_IWUSR)));
+            if (logFd.get() == -1) {
+                return ErrnoError() << "Failed to open " << mLogFile;
+            }
+        }
+
+        ScopedFileDescriptor apkFd(TEMP_FAILURE_RETRY(open(kConfigApkPath, O_RDONLY | O_CLOEXEC)));
+        if (apkFd.get() == -1) {
+            return ErrnoError() << "Failed to open config APK";
+        }
+
+        ScopedFileDescriptor idsigFd(
+                TEMP_FAILURE_RETRY(open(kConfigApkIdsigPath, O_RDONLY | O_CLOEXEC)));
+        if (idsigFd.get() == -1) {
+            return ErrnoError() << "Failed to open config APK signature";
+        }
+
+        ScopedFileDescriptor instanceFd(
+                TEMP_FAILURE_RETRY(open(mInstanceImageFile.c_str(), O_RDONLY | O_CLOEXEC)));
+        if (instanceFd.get() == -1) {
+            return ErrnoError() << "Failed to open instance image file";
+        }
+
+        auto config = VirtualMachineConfig::make<VirtualMachineConfig::Tag::appConfig>();
+        auto& appConfig = config.get<VirtualMachineConfig::Tag::appConfig>();
+        appConfig.apk = std::move(apkFd);
+        appConfig.idsig = std::move(idsigFd);
+        appConfig.instanceImage = std::move(instanceFd);
+        appConfig.configPath = kConfigFilePath;
+        appConfig.debug = false; // Don't disable selinux in VM
+        appConfig.memoryMib = 0; // Use default
+
+        LOG(INFO) << "Starting VM";
+        auto status = service->startVm(config, logFd, &mVm);
+        if (!status.isOk()) {
+            return Error() << status.getDescription();
+        }
+
+        int32_t cid;
+        status = mVm->getCid(&cid);
+        if (!status.isOk()) {
+            return Error() << status.getDescription();
+        }
+
+        LOG(INFO) << "Started VM with cid = " << cid;
+
+        // We need to use this rather than std::make_shared to make sure the
+        // embedded weak_ptr is initialised.
+        mCallback = SharedRefBase::make<Callback>();
+
+        status = mVm->registerCallback(mCallback);
+        if (!status.isOk()) {
+            return Error() << status.getDescription();
+        }
+
+        if (!mCallback->waitForStarted()) {
+            return Error() << "VM Payload failed to start";
+        }
+
+        // TODO(b/194677789): Implement a polling loop or find a more reliable
+        // way to detect when the service is listening.
+        sleep(3);
+
+        return cid;
+    }
+
+private:
+    const int mCid;
+    const std::string mLogFile;
+    const std::string mInstanceImageFile;
+    std::shared_ptr<Callback> mCallback;
+    std::shared_ptr<IVirtualMachine> mVm;
+};
+} // namespace
+
 static Result<std::vector<uint8_t>> extractRsaPublicKey(
         const std::vector<uint8_t>& der_certificate) {
     auto data = der_certificate.data();
@@ -102,9 +275,13 @@
     return result;
 }
 
-static Result<void> generate(int cid, const std::string& blob_file,
+static Result<void> generate(TargetVm& vm, const std::string& blob_file,
                              const std::string& public_key_file) {
-    auto service = getService(cid);
+    auto cid = vm.resolveCid();
+    if (!cid.ok()) {
+        return cid.error();
+    }
+    auto service = getService(*cid);
     if (!service) {
         return Error() << "No service";
     }
@@ -130,9 +307,13 @@
     return {};
 }
 
-static Result<bool> verify(int cid, const std::string& blob_file,
+static Result<bool> verify(TargetVm& vm, const std::string& blob_file,
                            const std::string& public_key_file) {
-    auto service = getService(cid);
+    auto cid = vm.resolveCid();
+    if (!cid.ok()) {
+        return cid.error();
+    }
+    auto service = getService(*cid);
     if (!service) {
         return Error() << "No service";
     }
@@ -223,9 +404,13 @@
     return {};
 }
 
-static Result<void> sign(int cid, const std::string& blob_file,
+static Result<void> sign(TargetVm& vm, const std::string& blob_file,
                          const std::vector<std::string>& files) {
-    auto service = getService(cid);
+    auto cid = vm.resolveCid();
+    if (!cid.ok()) {
+        return cid.error();
+    }
+    auto service = getService(*cid);
     if (!service) {
         return Error() << "No service";
     }
@@ -244,30 +429,63 @@
     return {};
 }
 
+static Result<void> makeInstanceImage(const std::string& image_path) {
+    ndk::SpAIBinder binder(AServiceManager_waitForService("android.system.virtualizationservice"));
+    auto service = IVirtualizationService::fromBinder(binder);
+    if (!service) {
+        return Error() << "Failed to connect to virtualization service.";
+    }
+
+    ScopedFileDescriptor fd(TEMP_FAILURE_RETRY(
+            open(image_path.c_str(), O_CREAT | O_RDWR | O_TRUNC | O_CLOEXEC, S_IRUSR | S_IWUSR)));
+    if (fd.get() == -1) {
+        return ErrnoError() << "Failed to create image file";
+    }
+
+    auto status = service->initializeWritablePartition(fd, 10 * 1024 * 1024);
+    if (!status.isOk()) {
+        return Error() << "Failed to initialize partition: " << status.getDescription();
+    }
+    return {};
+}
+
 int main(int argc, char** argv) {
     // Restrict access to our outputs to the current user.
     umask(077);
 
     int cid = 0;
-    if (argc >= 3 && argv[1] == "--cid"sv) {
-        cid = atoi(argv[2]);
-        if (cid == 0) {
-            std::cerr << "Invalid cid\n";
-            return 1;
+    std::string imageFile;
+    std::string logFile;
+
+    while (argc >= 3) {
+        if (argv[1] == "--cid"sv) {
+            cid = atoi(argv[2]);
+            if (cid == 0) {
+                std::cerr << "Invalid cid\n";
+                return 1;
+            }
+        } else if (argv[1] == "--start"sv) {
+            imageFile = argv[2];
+        } else if (argv[1] == "--log"sv) {
+            logFile = argv[2];
+        } else {
+            break;
         }
         argc -= 2;
         argv += 2;
     }
 
+    TargetVm vm(cid, logFile, imageFile);
+
     if (argc == 4 && argv[1] == "generate"sv) {
-        auto result = generate(cid, argv[2], argv[3]);
+        auto result = generate(vm, argv[2], argv[3]);
         if (result.ok()) {
             return 0;
         } else {
             std::cerr << result.error() << '\n';
         }
     } else if (argc == 4 && argv[1] == "verify"sv) {
-        auto result = verify(cid, argv[2], argv[3]);
+        auto result = verify(vm, argv[2], argv[3]);
         if (result.ok()) {
             if (result.value()) {
                 std::cerr << "Key files are valid.\n";
@@ -280,23 +498,35 @@
         }
     } else if (argc >= 4 && argv[1] == "sign"sv) {
         const std::vector<std::string> files{&argv[3], &argv[argc]};
-        auto result = sign(cid, argv[2], files);
+        auto result = sign(vm, argv[2], files);
         if (result.ok()) {
             std::cerr << "All signatures generated.\n";
             return 0;
         } else {
             std::cerr << result.error() << '\n';
         }
+    } else if (argc == 3 && argv[1] == "make-instance"sv) {
+        auto result = makeInstanceImage(argv[2]);
+        if (result.ok()) {
+            return 0;
+        } else {
+            std::cerr << result.error() << '\n';
+        }
     } else {
-        std::cerr << "Usage: compos_key_cmd [--cid <cid>] generate|verify|sign\n"
-                  << "  generate <blob file> <public key file> Generate new key pair and "
-                     "write\n"
+        std::cerr << "Usage: compos_key_cmd [OPTIONS] generate|verify|sign|make-instance\n"
+                  << "  generate <blob file> <public key file> Generate new key pair and write\n"
                   << "    the private key blob and public key to the specified files.\n "
                   << "  verify <blob file> <public key file> Verify that the content of the\n"
                   << "    specified private key blob and public key files are valid.\n "
                   << "  sign <blob file> <files to be signed> Generate signatures for one or\n"
                   << "    more files using the supplied private key blob.\n"
-                  << "Specify --cid to connect to a VM rather than the host\n";
+                  << "  make-instance <image file> Create an empty instance image file for a VM.\n"
+                  << "\n"
+                  << "OPTIONS: --log <log file> (--cid <cid> | --start <image file>)\n"
+                  << "  Specify --log to write VM log to a file rather than stdout.\n"
+                  << "  Specify --cid to connect to a VM rather than the host.\n"
+                  << "  Specify --start to start a VM from the given instance image file and\n "
+                  << "    connect to that.\n";
     }
     return 1;
 }
diff --git a/compos/tests/java/android/compos/test/ComposTestCase.java b/compos/tests/java/android/compos/test/ComposTestCase.java
index 4471e63..da23919 100644
--- a/compos/tests/java/android/compos/test/ComposTestCase.java
+++ b/compos/tests/java/android/compos/test/ComposTestCase.java
@@ -30,11 +30,13 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 @RootPermissionTest
 @RunWith(DeviceJUnit4ClassRunner.class)
+@Ignore("b/194974010")
 public final class ComposTestCase extends VirtualizationTestCaseBase {
 
     /** Path to odrefresh on Microdroid */
diff --git a/microdroid/microdroid.json b/microdroid/microdroid.json
index da82289..1337edf 100644
--- a/microdroid/microdroid.json
+++ b/microdroid/microdroid.json
@@ -5,23 +5,23 @@
       "partitions": [
         {
           "label": "boot_a",
-          "paths": ["/apex/com.android.virt/etc/fs/microdroid_boot-5.10.img"]
+          "path": "/apex/com.android.virt/etc/fs/microdroid_boot-5.10.img"
         },
         {
           "label": "vendor_boot_a",
-          "paths": ["/apex/com.android.virt/etc/fs/microdroid_vendor_boot-5.10.img"]
+          "path": "/apex/com.android.virt/etc/fs/microdroid_vendor_boot-5.10.img"
         },
         {
           "label": "vbmeta_a",
-          "paths": ["/apex/com.android.virt/etc/fs/microdroid_vbmeta.img"]
+          "path": "/apex/com.android.virt/etc/fs/microdroid_vbmeta.img"
         },
         {
           "label": "vbmeta_system_a",
-          "paths": ["/apex/com.android.virt/etc/fs/microdroid_vbmeta_system.img"]
+          "path": "/apex/com.android.virt/etc/fs/microdroid_vbmeta_system.img"
         },
         {
           "label": "super",
-          "paths": ["/apex/com.android.virt/etc/fs/microdroid_super.img"]
+          "path": "/apex/com.android.virt/etc/fs/microdroid_super.img"
         }
       ],
       "writable": false
@@ -30,7 +30,7 @@
       "partitions": [
         {
           "label": "uboot_env",
-          "paths": ["/apex/com.android.virt/etc/uboot_env.img"],
+          "path": "/apex/com.android.virt/etc/uboot_env.img",
           "writable": false
         }
       ],
diff --git a/virtualizationservice/aidl/Android.bp b/virtualizationservice/aidl/Android.bp
index f7cb339..7d85bd3 100644
--- a/virtualizationservice/aidl/Android.bp
+++ b/virtualizationservice/aidl/Android.bp
@@ -16,6 +16,11 @@
         cpp: {
             enabled: true,
         },
+        ndk: {
+            apex_available: [
+                "com.android.compos",
+            ],
+        },
         rust: {
             enabled: true,
             apex_available: ["com.android.virt"],
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/Partition.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/Partition.aidl
index 9b8658b..825c3da 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/Partition.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/Partition.aidl
@@ -20,8 +20,8 @@
     /** A label for the partition. */
     @utf8InCpp String label;
 
-    /** The backing file descriptors of the partition images. */
-    ParcelFileDescriptor[] images;
+    /** The backing file descriptor of the partition image. */
+    ParcelFileDescriptor image;
 
     /** Whether the partition should be writable by the VM. */
     boolean writable;
diff --git a/virtualizationservice/src/composite.rs b/virtualizationservice/src/composite.rs
index 378ed78..685d0e6 100644
--- a/virtualizationservice/src/composite.rs
+++ b/virtualizationservice/src/composite.rs
@@ -50,20 +50,14 @@
 const LINUX_FILESYSTEM_GUID: Uuid = Uuid::from_u128(0x0FC63DAF_8483_4772_8E79_3D69D8477DE4);
 const EFI_SYSTEM_PARTITION_GUID: Uuid = Uuid::from_u128(0xC12A7328_F81F_11D2_BA4B_00A0C93EC93B);
 
-/// Information about a single image file to be included in a partition.
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct PartitionFileInfo {
-    path: PathBuf,
-    size: u64,
-}
-
-/// Information about a partition to create, including the set of image files which make it up.
+/// Information about a partition to create.
 #[derive(Clone, Debug, Eq, PartialEq)]
 pub struct PartitionInfo {
     label: String,
-    files: Vec<PartitionFileInfo>,
+    path: PathBuf,
     partition_type: ImagePartitionType,
     writable: bool,
+    size: u64,
 }
 
 /// Round `val` up to the next multiple of 2**`align_log`.
@@ -79,7 +73,7 @@
 
 impl PartitionInfo {
     fn aligned_size(&self) -> u64 {
-        align_to_partition_size(self.files.iter().map(|file| file.size).sum())
+        align_to_partition_size(self.size)
     }
 }
 
@@ -172,26 +166,18 @@
 ) -> Result<Vec<ComponentDisk>, Error> {
     let aligned_size = partition.aligned_size();
 
-    if partition.files.is_empty() {
-        bail!("No image files for partition {:?}", partition);
-    }
-    let mut file_size_sum = 0;
-    let mut component_disks = vec![];
-    for file in &partition.files {
-        component_disks.push(ComponentDisk {
-            offset: offset + file_size_sum,
-            file_path: file.path.to_str().context("Invalid partition path")?.to_string(),
-            read_write_capability: if partition.writable {
-                ReadWriteCapability::READ_WRITE
-            } else {
-                ReadWriteCapability::READ_ONLY
-            },
-            ..ComponentDisk::new()
-        });
-        file_size_sum += file.size;
-    }
+    let mut component_disks = vec![ComponentDisk {
+        offset,
+        file_path: partition.path.to_str().context("Invalid partition path")?.to_string(),
+        read_write_capability: if partition.writable {
+            ReadWriteCapability::READ_WRITE
+        } else {
+            ReadWriteCapability::READ_ONLY
+        },
+        ..ComponentDisk::new()
+    }];
 
-    if file_size_sum != aligned_size {
+    if partition.size != aligned_size {
         if partition.writable {
             bail!(
                 "Read-write partition {:?} size is not a multiple of {}.",
@@ -207,7 +193,7 @@
                 1 << PARTITION_SIZE_SHIFT
             );
             component_disks.push(ComponentDisk {
-                offset: offset + file_size_sum,
+                offset: offset + partition.size,
                 file_path: zero_filler_path.to_owned(),
                 read_write_capability: ReadWriteCapability::READ_ONLY,
                 ..ComponentDisk::new()
@@ -348,7 +334,7 @@
     Ok((composite_image, files))
 }
 
-/// Given the AIDL config containing a list of partitions, with [`ParcelFileDescriptor`]s for each
+/// Given the AIDL config containing a list of partitions, with a [`ParcelFileDescriptor`] for each
 /// partition, return the list of file descriptors which must be passed to the composite disk image
 /// partition configuration for it.
 fn convert_partitions(partitions: &[Partition]) -> Result<(Vec<PartitionInfo>, Vec<File>), Error> {
@@ -358,29 +344,24 @@
     let partitions = partitions
         .iter()
         .map(|partition| {
-            let image_files = partition
-                .images
-                .iter()
-                .map(|image| {
-                    let file = image
-                        .as_ref()
-                        .try_clone()
-                        .context("Failed to clone partition image file descriptor")?;
-
-                    let size = get_partition_size(&file)?;
-                    let fd = file.as_raw_fd();
-                    let partition_info_file =
-                        PartitionFileInfo { path: format!("/proc/self/fd/{}", fd).into(), size };
-                    files.push(file);
-                    Ok(partition_info_file)
-                })
-                .collect::<Result<Vec<_>, Error>>()?;
+            // TODO(b/187187765): This shouldn't be an Option.
+            let file = partition
+                .image
+                .as_ref()
+                .context("Invalid partition image file descriptor")?
+                .as_ref()
+                .try_clone()
+                .context("Failed to clone partition image file descriptor")?;
+            let size = get_partition_size(&file)?;
+            let fd = file.as_raw_fd();
+            files.push(file);
 
             Ok(PartitionInfo {
                 label: partition.label.to_owned(),
-                files: image_files,
+                path: format!("/proc/self/fd/{}", fd).into(),
                 partition_type: ImagePartitionType::LinuxFilesystem,
                 writable: partition.writable,
+                size,
             })
         })
         .collect::<Result<_, Error>>()?;
diff --git a/virtualizationservice/src/payload.rs b/virtualizationservice/src/payload.rs
index a176e71..338e9a2 100644
--- a/virtualizationservice/src/payload.rs
+++ b/virtualizationservice/src/payload.rs
@@ -129,7 +129,7 @@
     // put metadata at the first partition
     let mut partitions = vec![Partition {
         label: "payload-metadata".to_owned(),
-        images: vec![metadata_file],
+        image: Some(metadata_file),
         writable: false,
     }];
 
@@ -139,18 +139,18 @@
         let apex_file = open_parcel_file(&apex_path, false)?;
         partitions.push(Partition {
             label: format!("microdroid-apex-{}", i),
-            images: vec![apex_file],
+            image: Some(apex_file),
             writable: false,
         });
     }
     partitions.push(Partition {
         label: "microdroid-apk".to_owned(),
-        images: vec![ParcelFileDescriptor::new(apk_file)],
+        image: Some(ParcelFileDescriptor::new(apk_file)),
         writable: false,
     });
     partitions.push(Partition {
         label: "microdroid-apk-idsig".to_owned(),
-        images: vec![ParcelFileDescriptor::new(idsig_file)],
+        image: Some(ParcelFileDescriptor::new(idsig_file)),
         writable: false,
     });
 
@@ -182,10 +182,10 @@
     if config.debug {
         vm_config.disks[1].partitions.push(Partition {
             label: "bootconfig".to_owned(),
-            images: vec![open_parcel_file(
+            image: Some(open_parcel_file(
                 Path::new("/apex/com.android.virt/etc/microdroid_bootconfig.debug"),
                 false,
-            )?],
+            )?),
             writable: false,
         });
     }
@@ -193,7 +193,7 @@
     // instance image is at the second partition in the second disk.
     vm_config.disks[1].partitions.push(Partition {
         label: "vm-instance".to_owned(),
-        images: vec![ParcelFileDescriptor::new(instance_file)],
+        image: Some(ParcelFileDescriptor::new(instance_file)),
         writable: true,
     });
 
diff --git a/vmconfig/src/lib.rs b/vmconfig/src/lib.rs
index 7739e36..3828742 100644
--- a/vmconfig/src/lib.rs
+++ b/vmconfig/src/lib.rs
@@ -131,8 +131,7 @@
     /// A label for the partition.
     pub label: String,
     /// The filename of the partition image.
-    #[serde(default)]
-    pub paths: Vec<PathBuf>,
+    pub path: PathBuf,
     /// Whether the partition should be writable.
     #[serde(default)]
     pub writable: bool,
@@ -140,15 +139,11 @@
 
 impl Partition {
     fn to_parcelable(&self) -> Result<AidlPartition> {
-        if self.paths.is_empty() {
-            bail!("Partition {} contains no paths", &self.label);
-        }
-        let images = self
-            .paths
-            .iter()
-            .map(|path| open_parcel_file(path, self.writable))
-            .collect::<Result<Vec<_>, _>>()?;
-        Ok(AidlPartition { images, writable: self.writable, label: self.label.to_owned() })
+        Ok(AidlPartition {
+            image: Some(open_parcel_file(&self.path, self.writable)?),
+            writable: self.writable,
+            label: self.label.to_owned(),
+        })
     }
 }