Merge "Add build.prop for microdroid"
diff --git a/apex/Android.bp b/apex/Android.bp
index bb73630..9c201b4 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -20,22 +20,42 @@
                 "authfs", // TODO(victorhsieh): move to microdroid once we can run the test in VM.
                 "crosvm",
             ],
+            filesystems: [
+                "microdroid_super",
+                "microdroid_boot-5.10",
+                "microdroid_vendor_boot-5.10",
+                "microdroid_vbmeta",
+                "microdroid_vbmeta_system",
+            ],
         },
         x86_64: {
             binaries: [
                 "authfs", // TODO(victorhsieh): move to microdroid once we can run the test in VM.
                 "crosvm",
             ],
+            filesystems: [
+                "microdroid_super",
+                "microdroid_boot-5.10",
+                "microdroid_vendor_boot-5.10",
+                "microdroid_vbmeta",
+                "microdroid_vbmeta_system",
+            ],
         },
     },
     binaries: [
-        "assemble_cvd",
         "fd_server",
+        "mk_cdisk",
+        "mk_microdroid_signature",
         "virtmanager",
         "vm",
     ],
-    filesystems: ["microdroid"],
-    prebuilts: ["com.android.virt.init.rc"],
+    prebuilts: [
+        "com.android.virt.init.rc",
+        "microdroid_cdisk.json",
+        "microdroid_cdisk_env.json",
+        "microdroid_uboot_env",
+        "microdroid_bootloader",
+    ],
     file_contexts: ":com.android.virt-file_contexts",
 }
 
diff --git a/authfs/TEST_MAPPING b/authfs/TEST_MAPPING
index d2dc1d8..d0c0b09 100644
--- a/authfs/TEST_MAPPING
+++ b/authfs/TEST_MAPPING
@@ -2,9 +2,6 @@
   "presubmit": [
     {
       "name": "authfs_device_test_src_lib"
-    },
-    {
-      "name": "AuthFsTestCases"
     }
   ]
 }
diff --git a/docs/getting_started/index.md b/docs/getting_started/index.md
index e43124c..9c06367 100644
--- a/docs/getting_started/index.md
+++ b/docs/getting_started/index.md
@@ -16,15 +16,14 @@
 [packages/modules/Virtualization](https://android.googlesource.com/platform/packages/modules/Virtualization)
 of the AOSP repository.
 
-### Host-side tests
+### Device-side tests
 
-These are tests where the test driver runs on the "host" (your computer) and it issues commands to
-the "target" (the connected device or emulator) over ADB. The tests spawn guest VMs and test
-different aspects of the architecture.
+The tests spawn guest VMs and test different aspects of the architecture.
 
 You can build and run them with:
-``` shell
-atest VirtualizationHostTestCases
+
+```shell
+atest VirtualizationTestCases
 ```
 
 If you run into problems, inspect the logs produced by `atest`. Their location is printed at the
@@ -35,36 +34,47 @@
 [CrosVM](https://android.googlesource.com/platform/external/crosvm/) is a Rust-based Virtual Machine
 Monitor (VMM) originally built for ChromeOS and ported to Android.
 
-It is not installed in regular Android builds (yet!), but it's installed in the
-VIM3L (yukawa) build, as part of the `com.android.virt` APEX.
-builds.
+It is not installed in regular Android builds (yet!), but it's installed in the VIM3L (yukawa)
+build, as part of the `com.android.virt` APEX.
 
 ### Spawning your own VMs
 
-You can spawn your own VMs by running CrosVM directly on a rooted KVM-enabled device. If your
-device is attached over ADB, you can run:
-``` shell
+You can spawn your own VMs by passing a JSON config file to the Virt Manager via the `vm` tool on a
+rooted KVM-enabled device. If your device is attached over ADB, you can run:
+
+```shell
+$ cat > vm_config.json
+{
+  "kernel": "/data/local/tmp/kernel",
+  "initrd": "/data/local/tmp/ramdisk",
+  "params": "rdinit=/bin/init"
+}
 $ adb root
 $ adb push <kernel> /data/local/tmp/kernel
 $ adb push <ramdisk> /data/local/tmp/ramdisk
-$ adb shell /apex/com.android.virt/bin/crosvm run --initrd /data/local/tmp/ramdisk /data/local/tmp/kernel
+$ adb push vm_config.json /data/local/tmp/vm_config.json
+$ adb shell "start virtmanager"
+$ adb shell "/apex/com.android.virt/bin/vm run /data/local/tmp/vm_config.json"
 ```
 
-### Building and updating CrosVM
+The `vm` command also has other subcommands for debugging; run `/apex/com.android.virt/bin/vm help`
+for details.
 
-You can update CrosVM by updating the `com.android.virt` APEX where CrosVM is
-in. If your device already has `com.android.virt` (e.g. VIM3L),
+### Building and updating CrosVM and Virt Manager
 
-``` shell
-$ m com.android.virt
-$ adb install out/target/product/<device_name>/system/apex/com.android.virt.apex
+You can update CrosVM and the Virt Manager service by updating the `com.android.virt` APEX. If your
+device already has `com.android.virt` (e.g. VIM3L):
+
+```shell
+$ TARGET_BUILD_APPS="com.android.virt" m
+$ adb install $ANDROID_PRODUCT_OUT/system/apex/com.android.virt.apex
 $ adb reboot
 ```
 
 If it doesn't have the APEX yet, you first need to place it manually to the
 system partition.
 
-``` shell
+```shell
 $ adb root
 $ adb disable-verity
 $ adb reboot
diff --git a/microdroid/Android.bp b/microdroid/Android.bp
index 5c9f144..7466a78 100644
--- a/microdroid/Android.bp
+++ b/microdroid/Android.bp
@@ -140,6 +140,14 @@
     ],
 }
 
+// TODO(jiyong): change the name to init, cause it's confusing
+microdroid_boot_cmdline = "rdinit=/bin/init_vendor " +
+    "panic=-1 " +
+    // TODO(b/181936135) make the ratelimiting conditional; ratelimiting on prod build
+    "printk.devkmsg=on " +
+    "androidboot.first_stage_console=1 " +
+    "androidboot.hardware=microdroid "
+
 bootimg {
     name: "microdroid_boot-5.10",
     ramdisk_module: "microdroid_ramdisk-5.10",
@@ -149,19 +157,16 @@
     arch: {
         arm64: {
             kernel_prebuilt: ":kernel_prebuilts-5.10-arm64",
+            cmdline: microdroid_boot_cmdline +
+                "androidboot.boot_devices=10000.pci",
         },
         x86_64: {
             kernel_prebuilt: ":kernel_prebuilts-5.10-x86_64",
+            cmdline: microdroid_boot_cmdline +
+                "pci=noacpi " +
+                "androidboot.boot_devices=pci0000:00/0000:00:01.0",
         },
     },
-    // TODO(jiyong): change the name to init, cause it's confusing
-    cmdline: "rdinit=/bin/init_vendor " +
-        "panic=-1 " +
-        // TODO(b/181936135) make the ratelimiting conditional; ratelimiting on prod build
-        "printk.devkmsg=on " +
-        "androidboot.first_stage_console=1 " +
-        "androidboot.hardware=microdroid " +
-        "androidboot.boot_devices=10000.pci ",
     dtb_prebuilt: "dummy_dtb.img",
     header_version: "4",
     partition_name: "boot",
@@ -233,8 +238,56 @@
 }
 
 prebuilt_etc {
+    name: "microdroid_bootloader",
+    src: ":microdroid_bootloader_gen",
+    arch: {
+        x86_64: {
+            // For unknown reason, the signed bootloader doesn't work on x86_64. Until the problem
+            // is fixed, let's use the unsigned bootloader for the architecture.
+            // TODO(b/185115783): remove this
+            src: ":cuttlefish_crosvm_bootloader",
+        },
+    },
+    filename: "microdroid_bootloader",
+}
+
+// See external/avb/avbtool.py
+// MAX_VBMETA_SIZE=64KB, MAX_FOOTER_SIZE=4KB
+avb_hash_footer_kb = "68"
+
+genrule {
+    name: "microdroid_bootloader_gen",
+    tools: ["avbtool"],
+    srcs: [
+        ":cuttlefish_crosvm_bootloader",
+        ":avb_testkey_rsa4096",
+    ],
+    out: ["bootloader-signed"],
+    // 1. Copy the input to the output becaise avbtool modifies --image in
+    // place.
+    // 2. Check if the file is big enough. For arm and x86 we have fake
+    // bootloader file whose size is 1. It can't pass avbtool.
+    // 3. Add the hash footer. The partition size is set to (image size + 68KB)
+    // rounded up to 4KB boundary.
+    cmd: "cp $(location :cuttlefish_crosvm_bootloader) $(out) && " +
+        "if [ $$(stat --format=%s $(out)) -gt 4096 ]; then " +
+        "$(location avbtool) add_hash_footer " +
+        "--algorithm SHA256_RSA4096 " +
+        "--partition_name bootloader " +
+        "--key $(location :avb_testkey_rsa4096) " +
+        "--partition_size $$(( " + avb_hash_footer_kb + " * 1024 + ( $$(stat --format=%s $(out)) + 4096 - 1 ) / 4096 * 4096 )) " +
+        "--image $(out)" +
+        "; fi",
+}
+
+prebuilt_etc {
     name: "microdroid_uboot_env",
     src: ":microdroid_uboot_env_gen",
+    arch: {
+        x86_64: {
+            src: ":microdroid_uboot_env_gen_x86_64",
+        },
+    },
     filename: "uboot_env.img",
 }
 
@@ -246,6 +299,14 @@
     cmd: "$(location mkenvimage_host) -s 4096 -o $(out) $(in)",
 }
 
+genrule {
+    name: "microdroid_uboot_env_gen_x86_64",
+    tools: ["mkenvimage_host"],
+    srcs: ["uboot-env-x86_64.txt"],
+    out: ["output.img"],
+    cmd: "$(location mkenvimage_host) -s 4096 -o $(out) $(in)",
+}
+
 // sepolicy sha256 for vendor
 prebuilt_etc {
     name: "microdroid_precompiled_sepolicy.plat_sepolicy_and_mapping.sha256",
@@ -306,3 +367,13 @@
         "microdroid",
     ],
 }
+
+prebuilt_etc {
+    name: "microdroid_cdisk.json",
+    src: "microdroid_cdisk.json",
+}
+
+prebuilt_etc {
+    name: "microdroid_cdisk_env.json",
+    src: "microdroid_cdisk_env.json",
+}
diff --git a/microdroid/README.md b/microdroid/README.md
index d737e22..a4bfb6e 100644
--- a/microdroid/README.md
+++ b/microdroid/README.md
@@ -10,52 +10,33 @@
 You need a VIM3L board. Instructions for building Android for the target, and
 flashing the image can be found [here](../docs/getting_started/yukawa.md).
 
-Then you build microdroid. Note that the instruction below is very likely to
-change in the future, because this is in active development. For example, the
-`microdroid_*` modules will eventually be included in the `com.android.virt`
-APEX, which is already in the `yukawa` (VIM3L) target.
+Then you install `com.android.virt` APEX. All files needed to run microdroid are
+included in the APEX, which is already in the `yukawa` (VIM3L) target. You can
+of course build and install the APEX manually.
 
 ```
 $ source build/envsetup.sh
 $ choosecombo 1 aosp_arm64 userdebug // actually, any arm64-based target is ok
-$ m microdroid_super
-$ m microdroid_boot-5.10
-$ m microdroid_vendor_boot-5.10
-$ m microdroid_uboot_env
-$ m microdroid_vbmeta
-$ m microdroid_vbmeta_system
-```
-
-## Installing
-
-Push the built files to the device. In addition to that, some other files have
-to be manually created, for now. In the future, you won't need these.
-
-```
-$ adb push device/google/cuttlefish_prebuilts/bootloader/crosvm_aarch64/u-boot.bin /data/local/tmp/bootloader
-$ adb push $ANDROID_PRODUCT_OUT/system/etc/microdroid_super.img /data/local/tmp/super.img
-$ adb push $ANDROID_PRODUCT_OUT/system/etc/microdroid_boot-5.10.img /data/local/tmp/boot.img
-$ adb push $ANDROID_PRODUCT_OUT/system/etc/microdroid_vendor_boot-5.10.img /data/local/tmp/vendor_boot.img
-$ adb push $ANDROID_PRODUCT_OUT/system/etc/microdroid_vbmeta.img /data/local/tmp/vbmeta.img
-$ adb push $ANDROID_PRODUCT_OUT/system/etc/microdroid_vbmeta_system.img /data/local/tmp/vbmeta_system.img
-$ adb shell mkdir /data/local/tmp/cuttlefish_runtime.1/
-$ adb push $ANDROID_PRODUCT_OUT/system/etc/uboot_env.img /data/local/tmp/cuttlefish_runtime.1/
-$ adb shell mkdir -p /data/local/tmp/etc/cvd_config
-$ adb shell 'echo "{}" > /data/local/tmp/etc/cvd_config/cvd_config_phone.json'
-$ dd if=/dev/zero of=empty.img bs=4k count=600
-$ mkfs.ext4 -F empty.img
-$ adb push empty.img /data/local/tmp/userdata.img
-$ adb push empty.img /data/local/tmp/cache.img
+$ m com.android.virt
+$ adb install $ANDROID_PRODUCT_OUT/system/apex/com.android.virt.apex
+$ adb reboot
 ```
 
 ## Running
 
-Create the composite image using `assemble_cvd` and run it via `crosvm`. In the
-future, this shall be done via [`virtmanager`](../virtmanager/).
+Copy the artifacts to the temp directory, create the composite image using
+`mk_cdisk`, and run it via `crosvm`. For now, some other files have to be
+manually created. In the future, you won't need these, and this shall be done
+via [`virtmanager`](../virtmanager/).
 
 ```
-$ adb shell 'HOME=/data/local/tmp; PATH=$PATH:/apex/com.android.virt/bin; assemble_cvd -protected_vm < /dev/null'
-$ adb shell 'cd /data/local/tmp; /apex/com.android.virt/bin/crosvm run --cid=5 --disable-sandbox --bios=bootloader --serial=type=stdout --disk=cuttlefish_runtime/os_composite.img'
+$ adb shell 'cp /apex/com.android.virt/etc/microdroid_bootloader /data/local/tmp/bootloader'
+$ adb shell 'cp /apex/com.android.virt/etc/fs/*.img /data/local/tmp'
+$ adb shell 'cp /apex/com.android.virt/etc/uboot_env.img /data/local/tmp'
+$ adb shell 'dd if=/dev/zero of=/data/local/tmp/misc.img bs=4k count=256'
+$ adb shell 'cd /data/local/tmp; /apex/com.android.virt/bin/mk_cdisk /apex/com.android.virt/etc/microdroid_cdisk.json os_composite.img'
+$ adb shell 'cd /data/local/tmp; /apex/com.android.virt/bin/mk_cdisk /apex/com.android.virt/etc/microdroid_cdisk_env.json env_composite.img'
+$ adb shell 'cd /data/local/tmp; /apex/com.android.virt/bin/crosvm run --cid=5 --disable-sandbox --bios=bootloader --serial=type=stdout --disk=os_composite.img --disk=env_composite.img'
 ```
 
 The CID in `--cid` parameter can be anything greater than 2 (`VMADDR_CID_HOST`).
diff --git a/microdroid/microdroid_cdisk.json b/microdroid/microdroid_cdisk.json
new file mode 100644
index 0000000..5721775
--- /dev/null
+++ b/microdroid/microdroid_cdisk.json
@@ -0,0 +1,44 @@
+{
+  "partitions": [
+    {
+      "label": "misc",
+      "path": "misc.img"
+    },
+    {
+      "label": "boot_a",
+      "path": "microdroid_boot-5.10.img"
+    },
+    {
+      "label": "boot_b",
+      "path": "microdroid_boot-5.10.img"
+    },
+    {
+      "label": "vendor_boot_a",
+      "path": "microdroid_vendor_boot-5.10.img"
+    },
+    {
+      "label": "vendor_boot_b",
+      "path": "microdroid_vendor_boot-5.10.img"
+    },
+    {
+      "label": "vbmeta_a",
+      "path": "microdroid_vbmeta.img"
+    },
+    {
+      "label": "vbmeta_b",
+      "path": "microdroid_vbmeta.img"
+    },
+    {
+      "label": "vbmeta_system_a",
+      "path": "microdroid_vbmeta_system.img"
+    },
+    {
+      "label": "vbmeta_system_b",
+      "path": "microdroid_vbmeta_system.img"
+    },
+    {
+      "label": "super",
+      "path": "microdroid_super.img"
+    }
+  ]
+}
diff --git a/microdroid/microdroid_cdisk_env.json b/microdroid/microdroid_cdisk_env.json
new file mode 100644
index 0000000..b0fbe44
--- /dev/null
+++ b/microdroid/microdroid_cdisk_env.json
@@ -0,0 +1,8 @@
+{
+  "partitions": [
+    {
+      "label": "uboot_env",
+      "path": "uboot_env.img"
+    }
+  ]
+}
diff --git a/microdroid/signature/Android.bp b/microdroid/signature/Android.bp
new file mode 100644
index 0000000..63c2128
--- /dev/null
+++ b/microdroid/signature/Android.bp
@@ -0,0 +1,49 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_defaults {
+    name: "microdroid_signature_default",
+    host_supported: true,
+    srcs: [
+        "microdroid_signature.proto",
+        "signature.cc",
+    ],
+    shared_libs: [
+        "libbase",
+        "liblog",
+    ],
+    export_include_dirs: ["include"],
+}
+
+cc_library_static {
+    name: "lib_microdroid_signature_proto_lite",
+    recovery_available: true,
+    proto: {
+        export_proto_headers: true,
+        type: "lite",
+    },
+    defaults: ["microdroid_signature_default"],
+    apex_available: [
+        "com.android.virt",
+    ],
+}
+
+cc_binary {
+    name: "mk_microdroid_signature",
+    srcs: [
+        "mk_microdroid_signature.cc",
+    ],
+    shared_libs: [
+        "libbase",
+        "liblog",
+    ],
+    static_libs: [
+        "lib_microdroid_signature_proto_lite",
+        "libjsoncpp",
+        "libprotobuf-cpp-lite",
+    ],
+    apex_available: [
+        "com.android.virt",
+    ],
+}
diff --git a/microdroid/signature/README.md b/microdroid/signature/README.md
new file mode 100644
index 0000000..4f434ee
--- /dev/null
+++ b/microdroid/signature/README.md
@@ -0,0 +1,56 @@
+# Microdroid Signature
+
+Microdroid Signature contains the signatures of the payloads so that the payloads are
+verified inside the Guest OS.
+
+* APEX packages that are passed to microdroid should be listed in the Microroid Signature.
+
+## Format
+
+Microdroid Signature is composed of header and body.
+
+| offset | size | description                                                    |
+|--------|------|----------------------------------------------------------------|
+| 0      | 4    | Header. unsigned int32: body length(L) in big endian           |
+| 4      | L    | Body. A protobuf message. [schema](microdroid_signature.proto) |
+
+## How to Create
+
+For testing purpose, use `mk_microdroid_signature` to create a Microdroid Signature.
+
+```
+$ cat signature_config.json
+{
+  "apexes": [
+    {
+      "name": "com.my.hello",
+      "path": "hello.apex"
+    }
+  ]
+}
+$ adb push signature_config.json hello.apex /data/local/tmp/
+$ adb shell 'cd /data/local/tmp; /apex/com.android.virt/bin/mk_microdroid_signature signature_config.json signature
+```
+
+Then, pass the signature as the first partition of the payload disk image.
+
+```
+$ cat payload_cdisk.json
+{
+  "partitions": [
+    {
+      "label": "signature",
+      "path": "signature"
+    },
+    {
+      "label": "com.my.hello",
+      "path": "hello.apex"
+    }
+  ]
+}
+$ adb push payload_cdisk.json /data/local/tmp/
+$ adb shell 'cd /data/local/tmp; /apex/com.android.virt/bin/mk_cdisk payload_cdisk.json payload.img
+$ adb shell 'cd /data/local/tmp; /apex/com.android.virt/bin/crosvm .... --disk=payload.img'
+```
+
+In the future, [VirtManager](../../virtmanager) will handle these stuffs.
\ No newline at end of file
diff --git a/microdroid/signature/include/microdroid/signature.h b/microdroid/signature/include/microdroid/signature.h
new file mode 100644
index 0000000..abacd6e
--- /dev/null
+++ b/microdroid/signature/include/microdroid/signature.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 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.
+ */
+
+#pragma once
+
+#include <android-base/result.h>
+#include <microdroid_signature.pb.h>
+
+#include <iostream>
+#include <string>
+
+namespace android {
+namespace microdroid {
+
+base::Result<MicrodroidSignature> ReadMicrodroidSignature(const std::string& path);
+
+base::Result<void> WriteMicrodroidSignature(const MicrodroidSignature& signature,
+                                            std::ostream& out);
+
+} // namespace microdroid
+} // namespace android
diff --git a/microdroid/signature/microdroid_signature.proto b/microdroid/signature/microdroid_signature.proto
new file mode 100644
index 0000000..8335ff5
--- /dev/null
+++ b/microdroid/signature/microdroid_signature.proto
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 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.
+ */
+
+syntax = "proto3";
+
+package android.microdroid;
+
+// Microdroid Signature is the body of the signature partition.
+message MicrodroidSignature {
+  uint32 version = 1;
+
+  // Lists the signature information of the payload apexes.
+  // The payload apexes are mapped to the partitions following the signature partition.
+  repeated ApexSignature apexes = 2;
+}
+
+message ApexSignature {
+  // Required.
+  // The apex name.
+  string name = 1;
+
+  // Required.
+  // The original size of the apex file.
+  uint32 size = 2;
+
+  // Optional.
+  // When specified, the public key used to sign the apex should match with it.
+  string publicKey = 3;
+
+  // Optional.
+  // When specified, the root digest of the apex should match with it.
+  string rootDigest = 4;
+}
diff --git a/microdroid/signature/mk_microdroid_signature.cc b/microdroid/signature/mk_microdroid_signature.cc
new file mode 100644
index 0000000..d5cb1a5
--- /dev/null
+++ b/microdroid/signature/mk_microdroid_signature.cc
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 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.
+ */
+
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <fstream>
+#include <iostream>
+#include <string>
+
+#include <android-base/file.h>
+#include <android-base/result.h>
+#include <json/json.h>
+
+#include "microdroid/signature.h"
+
+using android::base::Dirname;
+using android::base::ErrnoError;
+using android::base::Error;
+using android::base::Result;
+using android::microdroid::MicrodroidSignature;
+using android::microdroid::WriteMicrodroidSignature;
+
+Result<uint32_t> GetFileSize(const std::string& path) {
+    struct stat st;
+    if (lstat(path.c_str(), &st) == -1) {
+        return ErrnoError() << "Can't lstat " << path;
+    }
+    return static_cast<uint32_t>(st.st_size);
+}
+
+// config JSON schema:
+// {
+//   "apexes": [
+//     {
+//       "name": string,       // the apex name
+//       "path": string,       // the path to the apex file
+//                             // absolute or relative to the config file
+//       "publicKey": string,  // optional
+//       "rootDigest": string, // optional
+//     }
+//   ]
+// }
+
+Result<MicrodroidSignature> LoadConfig(const std::string& config_file) {
+    MicrodroidSignature signature;
+    signature.set_version(1);
+
+    const std::string dirname = Dirname(config_file);
+    std::ifstream in(config_file);
+    Json::CharReaderBuilder builder;
+    Json::Value root;
+    Json::String errs;
+    if (!parseFromStream(builder, in, &root, &errs)) {
+        return Error() << "bad config: " << errs;
+    }
+
+    for (const Json::Value& apex : root["apexes"]) {
+        auto apex_signature = signature.add_apexes();
+
+        Json::Value name = apex["name"];
+        Json::Value path = apex["path"];
+        Json::Value publicKey = apex["publicKey"];
+        Json::Value rootDigest = apex["rootDigest"];
+
+        if (name.isString()) {
+            apex_signature->set_name(name.asString());
+        } else {
+            return Error() << "bad config: apexes.name should be a string: " << path;
+        }
+
+        if (path.isString()) {
+            std::string apex_path = path.asString();
+
+            // resolve path with the config_file's dirname if not absolute
+            bool is_absolute = !apex_path.empty() && apex_path[0] == '/';
+            if (!is_absolute) {
+                apex_path = dirname + "/" + apex_path;
+            }
+
+            auto file_size = GetFileSize(apex_path);
+            if (!file_size.ok()) {
+                return Error() << "I/O error: " << file_size.error();
+            }
+            apex_signature->set_size(file_size.value());
+        } else {
+            return Error() << "bad config: apexes.path should be a string: " << path;
+        }
+
+        if (publicKey.isString()) {
+            apex_signature->set_publickey(publicKey.asString());
+        } else if (!publicKey.isNull()) {
+            return Error() << "bad config: apexes.publicKey should be a string or null: "
+                           << publicKey;
+        }
+
+        if (rootDigest.isString()) {
+            apex_signature->set_rootdigest(rootDigest.asString());
+        } else if (!rootDigest.isNull()) {
+            return Error() << "bad config: apexes.rootDigest should be a string or null: "
+                           << rootDigest;
+        }
+    }
+
+    return signature;
+}
+
+int main(int argc, char** argv) {
+    if (argc != 3) {
+        std::cerr << "Usage: " << argv[0] << " <config> <output>\n";
+        return 1;
+    }
+
+    auto config = LoadConfig(argv[1]);
+    if (!config.ok()) {
+        std::cerr << config.error() << '\n';
+        return 1;
+    }
+
+    std::ofstream out(argv[2]);
+    auto result = WriteMicrodroidSignature(*config, out);
+    if (!result.ok()) {
+        std::cerr << result.error() << '\n';
+        return 1;
+    }
+    return 0;
+}
\ No newline at end of file
diff --git a/microdroid/signature/signature.cc b/microdroid/signature/signature.cc
new file mode 100644
index 0000000..446159e
--- /dev/null
+++ b/microdroid/signature/signature.cc
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 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.
+ */
+
+#include "microdroid/signature.h"
+
+#include <android-base/endian.h>
+#include <android-base/file.h>
+
+using android::base::ErrnoError;
+using android::base::Error;
+using android::base::Result;
+
+namespace android {
+namespace microdroid {
+
+Result<MicrodroidSignature> ReadMicrodroidSignature(const std::string& path) {
+    std::string content;
+    if (!base::ReadFileToString(path, &content)) {
+        return ErrnoError() << "Failed to read " << path;
+    }
+
+    // read length prefix (4-byte, big-endian)
+    uint32_t size;
+    const size_t length_prefix_bytes = sizeof(size);
+    if (content.size() < length_prefix_bytes) {
+        return Error() << "Invalid signature: size == " << content.size();
+    }
+    size = be32toh(*reinterpret_cast<uint32_t*>(content.data()));
+    if (content.size() < length_prefix_bytes + size) {
+        return Error() << "Invalid signature: size(" << size << ") mimatches to the content size("
+                       << content.size() - length_prefix_bytes << ")";
+    }
+    content = content.substr(length_prefix_bytes, size);
+
+    // parse content
+    MicrodroidSignature signature;
+    if (!signature.ParseFromString(content)) {
+        return Error() << "Can't parse MicrodroidSignature from " << path;
+    }
+    return signature;
+}
+
+Result<void> WriteMicrodroidSignature(const MicrodroidSignature& signature, std::ostream& out) {
+    // prepare content
+    std::string content;
+    if (!signature.SerializeToString(&content)) {
+        return Error() << "Failed to write protobuf.";
+    }
+
+    // write length prefix (4-byte, big-endian)
+    uint32_t size = htobe32(static_cast<uint32_t>(content.size()));
+    out.write(reinterpret_cast<const char*>(&size), sizeof(size));
+
+    // write content
+    out << content;
+
+    return {};
+}
+
+} // namespace microdroid
+} // namespace android
\ No newline at end of file
diff --git a/microdroid/uboot-env-x86_64.txt b/microdroid/uboot-env-x86_64.txt
new file mode 100644
index 0000000..ffc0462
--- /dev/null
+++ b/microdroid/uboot-env-x86_64.txt
@@ -0,0 +1,13 @@
+# Static u-boot environment variables for microdroid. See b/180481192
+
+# Boot the device following the Android boot procedure
+bootcmd=avb init virtio 0 && avb verify _a && boot_android virtio 0#misc
+
+bootdelay=0
+
+# U-Boot in x86_64 by defaults loads kernel at 0x20000000 (512MB), which is
+# out of the physical memory when the VM is launched with the default memory
+# size of 256MB. To avoid that, explicitly set the kernel load addresss using
+# loadaddr variable.
+loadaddr=0x02000000
+fdtaddr=0x40000000
diff --git a/microdroid/uboot-env.txt b/microdroid/uboot-env.txt
index b7940e5..0bdc591 100644
--- a/microdroid/uboot-env.txt
+++ b/microdroid/uboot-env.txt
@@ -1,7 +1,7 @@
 # Static u-boot environment variables for microdroid. See b/180481192
 
 # Boot the device following the Android boot procedure
-bootcmd=boot_android virtio 0#misc
+bootcmd=avb init virtio 0 && avb verify _a && boot_android virtio 0#misc
 
 bootdelay=0
 fdtaddr=0x80000000
diff --git a/tests/hostside/Android.bp b/tests/hostside/Android.bp
index c030e8d..429b737 100644
--- a/tests/hostside/Android.bp
+++ b/tests/hostside/Android.bp
@@ -7,23 +7,4 @@
     srcs: ["java/**/*.java"],
     test_suites: ["device-tests"],
     libs: ["tradefed"],
-    data: [
-        ":microdroid_super",
-        ":microdroid_boot-5.10",
-        ":microdroid_vendor_boot-5.10",
-        ":microdroid_uboot_env",
-        ":cuttlefish_crosvm_bootloader",
-        ":MicrodroidHostTestCase_EmptyImage",
-        ":microdroid_vbmeta",
-        ":microdroid_vbmeta_system",
-    ],
-}
-
-genrule {
-    name: "MicrodroidHostTestCase_EmptyImage",
-    tools: ["mke2fs"],
-    out: ["empty.img"],
-    cmd: "dd if=/dev/zero of=$(out) bs=4k count=600 &&" +
-        "$(location mke2fs) -t ext4 $(out)",
-    visibility: ["//visibility:private"],
 }
diff --git a/tests/hostside/java/android/virt/test/MicrodroidTestCase.java b/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
index c5c86a6..57c7b17 100644
--- a/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
+++ b/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
@@ -30,9 +30,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import java.io.File;
-import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
@@ -45,17 +42,6 @@
     private static final String MICRODROID_SERIAL = "localhost:" + TEST_VM_ADB_PORT;
     private static final long MICRODROID_BOOT_TIMEOUT_MILLIS = 15000;
 
-    private void pushFile(String localName, String remoteName) {
-        try {
-            File localFile = getTestInformation().getDependencyFile(localName, false);
-            Path remotePath = Paths.get(TEST_ROOT, remoteName);
-            getDevice().executeShellCommand("mkdir -p " + remotePath.getParent());
-            getDevice().pushFile(localFile, remotePath.toString());
-        } catch (Exception e) {
-            throw new RuntimeException(e);
-        }
-    }
-
     private String executeCommand(String cmd) {
         final long defaultCommandTimeoutMillis = 1000; // 1 sec
         return executeCommand(defaultCommandTimeoutMillis, cmd);
@@ -69,50 +55,65 @@
     @Test
     public void testMicrodroidBoots() throws Exception {
         // Prepare input files
-        pushFile("u-boot.bin", "bootloader");
-        pushFile("microdroid_super.img", "super.img");
-        pushFile("microdroid_boot-5.10.img", "boot.img");
-        pushFile("microdroid_vendor_boot-5.10.img", "vendor_boot.img");
-        pushFile("uboot_env.img", "cuttlefish_runtime.1/uboot_env.img");
-        pushFile("empty.img", "userdata.img");
-        pushFile("microdroid_vbmeta.img", "vbmeta.img");
-        pushFile("microdroid_vbmeta_system.img", "vbmeta_system.img");
-        pushFile("empty.img", "cache.img");
-        getDevice().executeShellCommand("mkdir -p " + TEST_ROOT + "etc/cvd_config");
-        getDevice().pushString("{}", TEST_ROOT + "etc/cvd_config/cvd_config_phone.json");
+        String prepareImagesCmd =
+                String.format(
+                        "mkdir -p %s; cd %s; "
+                                + "cp %setc/microdroid_bootloader bootloader && "
+                                + "cp %setc/fs/*.img . && "
+                                + "cp %setc/uboot_env.img . && "
+                                + "dd if=/dev/zero of=misc.img bs=4k count=256",
+                        TEST_ROOT, TEST_ROOT, VIRT_APEX, VIRT_APEX, VIRT_APEX);
+        getDevice().executeShellCommand(prepareImagesCmd);
 
-        // Run assemble_cvd to create os_composite.img
-        getDevice().executeShellCommand("HOME=" + TEST_ROOT + "; "
-                + "PATH=$PATH:" + VIRT_APEX + "bin; "
-                + VIRT_APEX + "bin/assemble_cvd -protected_vm < /dev/null");
+        // Create os_composite.img and env_composite.img
+        String makeOsCompositeCmd =
+                String.format(
+                        "cd %s; %sbin/mk_cdisk %setc/microdroid_cdisk.json os_composite.img",
+                        TEST_ROOT, VIRT_APEX, VIRT_APEX);
+        getDevice().executeShellCommand(makeOsCompositeCmd);
+        String makeEnvCompositeCmd =
+                String.format(
+                        "cd %s; %sbin/mk_cdisk %setc/microdroid_cdisk_env.json env_composite.img",
+                        TEST_ROOT, VIRT_APEX, VIRT_APEX);
+        getDevice().executeShellCommand(makeEnvCompositeCmd);
 
-        // Make sure that os_composite.img is created
-        final String compositeImg = TEST_ROOT + "cuttlefish_runtime/os_composite.img";
-        CommandResult result = getDevice().executeShellV2Command("du -b " + compositeImg);
+        // Make sure that the composite images are created
+        final String compositeImg = TEST_ROOT + "/os_composite.img";
+        final String envCompositeImg = TEST_ROOT + "/env_composite.img";
+        CommandResult result =
+                getDevice().executeShellV2Command("du -b " + compositeImg + " " + envCompositeImg);
         assertThat(result.getExitCode(), is(0));
         assertThat(result.getStdout(), is(not("")));
 
         // Start microdroid using crosvm
         ExecutorService executor = Executors.newFixedThreadPool(1);
-        executor.execute(() -> {
-            try {
-                getDevice().executeShellV2Command("cd " + TEST_ROOT + "; "
-                        + VIRT_APEX + "bin/crosvm run "
-                        + "--cid=" + TEST_VM_CID + " "
-                        + "--disable-sandbox "
-                        + "--bios=bootloader "
-                        + "--serial=type=syslog "
-                        + "--disk=cuttlefish_runtime/os_composite.img");
-            } catch (Exception e) {
-                throw new RuntimeException(e);
-            }
-        });
+        String runMicrodroidCmd =
+                String.format(
+                        "cd %s; %sbin/crosvm run --cid=%d --disable-sandbox --bios=bootloader"
+                                + " --serial=type=syslog --disk=os_composite.img"
+                                + " --disk=env_composite.img",
+                        TEST_ROOT, VIRT_APEX, TEST_VM_CID);
+        executor.execute(
+                () -> {
+                    try {
+                        getDevice().executeShellV2Command(runMicrodroidCmd);
+                    } catch (Exception e) {
+                        throw new RuntimeException(e);
+                    }
+                });
         // .. and wait for microdroid to boot
         // TODO(jiyong): don't wait too long. We can wait less by monitoring log from microdroid
         Thread.sleep(MICRODROID_BOOT_TIMEOUT_MILLIS);
 
         // Connect to microdroid and read a system property from there
-        executeCommand("adb forward tcp:" + TEST_VM_ADB_PORT + " vsock:" + TEST_VM_CID + ":5555");
+        executeCommand(
+                "adb -s "
+                        + getDevice().getSerialNumber()
+                        + " forward tcp:"
+                        + TEST_VM_ADB_PORT
+                        + " vsock:"
+                        + TEST_VM_CID
+                        + ":5555");
         executeCommand("adb connect " + MICRODROID_SERIAL);
         String prop = executeCommand("adb -s " + MICRODROID_SERIAL + " shell getprop ro.hardware");
         assertThat(prop, is("microdroid"));
diff --git a/virtmanager/Android.bp b/virtmanager/Android.bp
index 1b2aec1..f1971dc 100644
--- a/virtmanager/Android.bp
+++ b/virtmanager/Android.bp
@@ -7,13 +7,14 @@
     crate_name: "virtmanager",
     srcs: ["src/main.rs"],
     edition: "2018",
+    prefer_rlib: true,
     rustlibs: [
         "android.system.virtmanager-rust",
         "libandroid_logger",
-        "libbinder_rs", // TODO(dbrazdil): remove once b/182890877 is fixed
         "liblog_rust",
         "libserde_json",
         "libserde",
+        "libshared_child",
         "libanyhow",
     ],
     apex_available: ["com.android.virt"],
diff --git a/virtmanager/aidl/android/system/virtmanager/IVirtualMachine.aidl b/virtmanager/aidl/android/system/virtmanager/IVirtualMachine.aidl
index 0358bfd..26aad0c 100644
--- a/virtmanager/aidl/android/system/virtmanager/IVirtualMachine.aidl
+++ b/virtmanager/aidl/android/system/virtmanager/IVirtualMachine.aidl
@@ -15,7 +15,18 @@
  */
 package android.system.virtmanager;
 
+import android.system.virtmanager.IVirtualMachineCallback;
+
 interface IVirtualMachine {
     /** Get the CID allocated to the VM. */
     int getCid();
+
+    /** Returns true if the VM is still running, or false if it has exited for any reason. */
+    boolean isRunning();
+
+    /**
+     * Register a Binder object to get callbacks when the state of the VM changes, such as if it
+     * dies.
+     */
+    void registerCallback(IVirtualMachineCallback callback);
 }
diff --git a/virtmanager/aidl/android/system/virtmanager/IVirtualMachineCallback.aidl b/virtmanager/aidl/android/system/virtmanager/IVirtualMachineCallback.aidl
new file mode 100644
index 0000000..65a685d
--- /dev/null
+++ b/virtmanager/aidl/android/system/virtmanager/IVirtualMachineCallback.aidl
@@ -0,0 +1,32 @@
+/*
+ * 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.virtmanager;
+
+import android.system.virtmanager.IVirtualMachine;
+
+/**
+ * An object which a client may register with the Virt Manager to get callbacks about the state of
+ * a particular VM.
+ */
+oneway interface IVirtualMachineCallback {
+    /**
+     * Called when the VM dies.
+     *
+     * Note that this will not be called if the Virt Manager itself dies, so you should also use
+     * `link_to_death` to handle that.
+     */
+    void onDied(int cid);
+}
diff --git a/virtmanager/aidl/android/system/virtmanager/VirtualMachineDebugInfo.aidl b/virtmanager/aidl/android/system/virtmanager/VirtualMachineDebugInfo.aidl
index d1ba9a6..7bb77ce 100644
--- a/virtmanager/aidl/android/system/virtmanager/VirtualMachineDebugInfo.aidl
+++ b/virtmanager/aidl/android/system/virtmanager/VirtualMachineDebugInfo.aidl
@@ -19,13 +19,19 @@
 parcelable VirtualMachineDebugInfo {
     /** The CID assigned to the VM. */
     int cid;
+
     /** The UID of the process which requested the VM. */
-    int requester_uid;
+    int requesterUid;
+
     /** The SID of the process which requested the VM. */
-    @nullable String requester_sid;
+    @nullable String requesterSid;
+
     /**
      * The PID of the process which requested the VM. Note that this process may no longer exist and
      * the PID may have been reused for a different process, so this should not be trusted.
      */
-    int requester_pid;
+    int requesterPid;
+
+    /** Whether the VM is still running. */
+    boolean running;
 }
diff --git a/virtmanager/src/aidl.rs b/virtmanager/src/aidl.rs
index 98af714..5a4eedc 100644
--- a/virtmanager/src/aidl.rs
+++ b/virtmanager/src/aidl.rs
@@ -17,16 +17,16 @@
 use crate::config::VmConfig;
 use crate::crosvm::VmInstance;
 use crate::{Cid, FIRST_GUEST_CID};
-use ::binder::FromIBinder; // TODO(dbrazdil): remove once b/182890877 is fixed
 use android_system_virtmanager::aidl::android::system::virtmanager::IVirtManager::IVirtManager;
 use android_system_virtmanager::aidl::android::system::virtmanager::IVirtualMachine::{
     BnVirtualMachine, IVirtualMachine,
 };
+use android_system_virtmanager::aidl::android::system::virtmanager::IVirtualMachineCallback::IVirtualMachineCallback;
 use android_system_virtmanager::aidl::android::system::virtmanager::VirtualMachineDebugInfo::VirtualMachineDebugInfo;
 use android_system_virtmanager::binder::{
     self, Interface, ParcelFileDescriptor, StatusCode, Strong, ThreadState,
 };
-use log::error;
+use log::{debug, error};
 use std::ffi::CStr;
 use std::fs::File;
 use std::sync::{Arc, Mutex, Weak};
@@ -55,7 +55,6 @@
         log_fd: Option<&ParcelFileDescriptor>,
     ) -> binder::Result<Strong<dyn IVirtualMachine>> {
         let state = &mut *self.state.lock().unwrap();
-        let cid = state.next_cid;
         let log_fd = log_fd
             .map(|fd| fd.as_ref().try_clone().map_err(|_| StatusCode::UNKNOWN_ERROR))
             .transpose()?;
@@ -70,16 +69,9 @@
             })
         });
         let requester_pid = ThreadState::get_calling_pid();
-        let instance = Arc::new(start_vm(
-            config_fd.as_ref(),
-            cid,
-            log_fd,
-            requester_uid,
-            requester_sid,
-            requester_pid,
-        )?);
-        // TODO(qwandor): keep track of which CIDs are currently in use so that we can reuse them.
-        state.next_cid = state.next_cid.checked_add(1).ok_or(StatusCode::UNKNOWN_ERROR)?;
+        let cid = state.allocate_cid()?;
+        let instance =
+            start_vm(config_fd.as_ref(), cid, log_fd, requester_uid, requester_sid, requester_pid)?;
         state.add_vm(Arc::downgrade(&instance));
         Ok(VirtualMachine::create(instance))
     }
@@ -97,9 +89,10 @@
             .into_iter()
             .map(|vm| VirtualMachineDebugInfo {
                 cid: vm.cid as i32,
-                requester_uid: vm.requester_uid as i32,
-                requester_sid: vm.requester_sid.clone(),
-                requester_pid: vm.requester_pid,
+                requesterUid: vm.requester_uid as i32,
+                requesterSid: vm.requester_sid.clone(),
+                requesterPid: vm.requester_pid,
+                running: vm.running(),
             })
             .collect();
         Ok(cids)
@@ -107,16 +100,13 @@
 
     /// Hold a strong reference to a VM in Virt Manager. This method is only intended for debug
     /// purposes, and as such is only permitted from the shell user.
-    fn debugHoldVmRef(&self, vmref: &dyn IVirtualMachine) -> binder::Result<()> {
+    fn debugHoldVmRef(&self, vmref: &Strong<dyn IVirtualMachine>) -> binder::Result<()> {
         if !debug_access_allowed() {
             return Err(StatusCode::PERMISSION_DENIED.into());
         }
 
-        // Workaround for b/182890877.
-        let vm: Strong<dyn IVirtualMachine> = FromIBinder::try_from(vmref.as_binder()).unwrap();
-
         let state = &mut *self.state.lock().unwrap();
-        state.debug_hold_vm(vm);
+        state.debug_hold_vm(vmref.clone());
         Ok(())
     }
 
@@ -159,6 +149,48 @@
     fn getCid(&self) -> binder::Result<i32> {
         Ok(self.instance.cid as i32)
     }
+
+    fn isRunning(&self) -> binder::Result<bool> {
+        Ok(self.instance.running())
+    }
+
+    fn registerCallback(
+        &self,
+        callback: &Strong<dyn IVirtualMachineCallback>,
+    ) -> binder::Result<()> {
+        // TODO: Should this give an error if the VM is already dead?
+        self.instance.callbacks.add(callback.clone());
+        Ok(())
+    }
+}
+
+impl Drop for VirtualMachine {
+    fn drop(&mut self) {
+        debug!("Dropping {:?}", self);
+        self.instance.kill();
+    }
+}
+
+/// A set of Binders to be called back in response to various events on the VM, such as when it
+/// dies.
+#[derive(Debug, Default)]
+pub struct VirtualMachineCallbacks(Mutex<Vec<Strong<dyn IVirtualMachineCallback>>>);
+
+impl VirtualMachineCallbacks {
+    /// Call all registered callbacks to say that the VM has died.
+    pub fn callback_on_died(&self, cid: Cid) {
+        let callbacks = &*self.0.lock().unwrap();
+        for callback in callbacks {
+            if let Err(e) = callback.onDied(cid as i32) {
+                error!("Error calling callback: {}", e);
+            }
+        }
+    }
+
+    /// Add a new callback to the set.
+    fn add(&self, callback: Strong<dyn IVirtualMachineCallback>) {
+        self.0.lock().unwrap().push(callback);
+    }
 }
 
 /// The mutable state of the Virt Manager. There should only be one instance of this struct.
@@ -179,7 +211,7 @@
 }
 
 impl State {
-    /// Get a list of VMs which are currently running.
+    /// Get a list of VMs which still have Binder references to them.
     fn vms(&self) -> Vec<Arc<VmInstance>> {
         // Attempt to upgrade the weak pointers to strong pointers.
         self.vms.iter().filter_map(Weak::upgrade).collect()
@@ -204,6 +236,14 @@
         let pos = self.debug_held_vms.iter().position(|vm| vm.getCid() == Ok(cid))?;
         Some(self.debug_held_vms.swap_remove(pos))
     }
+
+    /// Get the next available CID, or an error if we have run out.
+    fn allocate_cid(&mut self) -> binder::Result<Cid> {
+        // TODO(qwandor): keep track of which CIDs are currently in use so that we can reuse them.
+        let cid = self.next_cid;
+        self.next_cid = self.next_cid.checked_add(1).ok_or(StatusCode::UNKNOWN_ERROR)?;
+        Ok(cid)
+    }
 }
 
 impl Default for State {
@@ -221,7 +261,7 @@
     requester_uid: u32,
     requester_sid: Option<String>,
     requester_pid: i32,
-) -> binder::Result<VmInstance> {
+) -> binder::Result<Arc<VmInstance>> {
     let config = VmConfig::load(config_file).map_err(|e| {
         error!("Failed to load VM config from {:?}: {:?}", config_file, e);
         StatusCode::BAD_VALUE
diff --git a/virtmanager/src/crosvm.rs b/virtmanager/src/crosvm.rs
index bef9982..5e6f658 100644
--- a/virtmanager/src/crosvm.rs
+++ b/virtmanager/src/crosvm.rs
@@ -14,12 +14,17 @@
 
 //! Functions for running instances of `crosvm`.
 
+use crate::aidl::VirtualMachineCallbacks;
 use crate::config::VmConfig;
 use crate::Cid;
 use anyhow::Error;
-use log::{debug, error, info};
+use log::{error, info};
+use shared_child::SharedChild;
 use std::fs::File;
-use std::process::{Child, Command};
+use std::process::Command;
+use std::sync::atomic::{AtomicBool, Ordering};
+use std::sync::Arc;
+use std::thread;
 
 const CROSVM_PATH: &str = "/apex/com.android.virt/bin/crosvm";
 
@@ -27,7 +32,7 @@
 #[derive(Debug)]
 pub struct VmInstance {
     /// The crosvm child process.
-    child: Child,
+    child: SharedChild,
     /// The CID assigned to the VM for vsock communication.
     pub cid: Cid,
     /// The UID of the process which requested the VM.
@@ -37,18 +42,30 @@
     /// The PID of the process which requested the VM. Note that this process may no longer exist
     /// and the PID may have been reused for a different process, so this should not be trusted.
     pub requester_pid: i32,
+    /// Whether the VM is still running.
+    running: AtomicBool,
+    /// Callbacks to clients of the VM.
+    pub callbacks: VirtualMachineCallbacks,
 }
 
 impl VmInstance {
     /// Create a new `VmInstance` for the given process.
     fn new(
-        child: Child,
+        child: SharedChild,
         cid: Cid,
         requester_uid: u32,
         requester_sid: Option<String>,
         requester_pid: i32,
     ) -> VmInstance {
-        VmInstance { child, cid, requester_uid, requester_sid, requester_pid }
+        VmInstance {
+            child,
+            cid,
+            requester_uid,
+            requester_sid,
+            requester_pid,
+            running: AtomicBool::new(true),
+            callbacks: Default::default(),
+        }
     }
 
     /// Start an instance of `crosvm` to manage a new VM. The `crosvm` instance will be killed when
@@ -60,29 +77,46 @@
         requester_uid: u32,
         requester_sid: Option<String>,
         requester_pid: i32,
-    ) -> Result<VmInstance, Error> {
+    ) -> Result<Arc<VmInstance>, Error> {
         let child = run_vm(config, cid, log_fd)?;
-        Ok(VmInstance::new(child, cid, requester_uid, requester_sid, requester_pid))
-    }
-}
+        let instance =
+            Arc::new(VmInstance::new(child, cid, requester_uid, requester_sid, requester_pid));
 
-impl Drop for VmInstance {
-    fn drop(&mut self) {
-        debug!("Dropping {:?}", self);
+        let instance_clone = instance.clone();
+        thread::spawn(move || {
+            instance_clone.monitor();
+        });
+
+        Ok(instance)
+    }
+
+    /// Wait for the crosvm child process to finish, then mark the VM as no longer running and call
+    /// any callbacks.
+    fn monitor(&self) {
+        match self.child.wait() {
+            Err(e) => error!("Error waiting for crosvm instance to die: {}", e),
+            Ok(status) => info!("crosvm exited with status {}", status),
+        }
+        self.running.store(false, Ordering::Release);
+        self.callbacks.callback_on_died(self.cid);
+    }
+
+    /// Return whether `crosvm` is still running the VM.
+    pub fn running(&self) -> bool {
+        self.running.load(Ordering::Acquire)
+    }
+
+    /// Kill the crosvm instance.
+    pub fn kill(&self) {
         // TODO: Talk to crosvm to shutdown cleanly.
         if let Err(e) = self.child.kill() {
             error!("Error killing crosvm instance: {}", e);
         }
-        // We need to wait on the process after killing it to avoid zombies.
-        match self.child.wait() {
-            Err(e) => error!("Error waiting for crosvm instance to die: {}", e),
-            Ok(status) => info!("Crosvm exited with status {}", status),
-        }
     }
 }
 
 /// Start an instance of `crosvm` to manage a new VM.
-fn run_vm(config: &VmConfig, cid: Cid, log_fd: Option<File>) -> Result<Child, Error> {
+fn run_vm(config: &VmConfig, cid: Cid, log_fd: Option<File>) -> Result<SharedChild, Error> {
     config.validate()?;
 
     let mut command = Command::new(CROSVM_PATH);
@@ -110,6 +144,5 @@
         command.arg(kernel);
     }
     info!("Running {:?}", command);
-    // TODO: Monitor child process, and remove from VM map if it dies.
-    Ok(command.spawn()?)
+    Ok(SharedChild::spawn(&mut command)?)
 }
diff --git a/vm/src/main.rs b/vm/src/main.rs
index 1f39e4a..061cfd7 100644
--- a/vm/src/main.rs
+++ b/vm/src/main.rs
@@ -17,14 +17,19 @@
 mod sync;
 
 use android_system_virtmanager::aidl::android::system::virtmanager::IVirtManager::IVirtManager;
+use android_system_virtmanager::aidl::android::system::virtmanager::IVirtualMachine::IVirtualMachine;
+use android_system_virtmanager::aidl::android::system::virtmanager::IVirtualMachineCallback::{
+    BnVirtualMachineCallback, IVirtualMachineCallback,
+};
 use android_system_virtmanager::binder::{
     get_interface, DeathRecipient, IBinder, ParcelFileDescriptor, ProcessState, Strong,
 };
+use android_system_virtmanager::binder::{Interface, Result as BinderResult};
 use anyhow::{Context, Error};
 use std::fs::File;
 use std::io;
 use std::os::unix::io::{AsRawFd, FromRawFd};
-use std::path::PathBuf;
+use std::path::{Path, PathBuf};
 use structopt::clap::AppSettings;
 use structopt::StructOpt;
 use sync::AtomicFlag;
@@ -73,7 +78,7 @@
 /// Run a VM from the given configuration file.
 fn command_run(
     virt_manager: Strong<dyn IVirtManager>,
-    config_path: &PathBuf,
+    config_path: &Path,
     daemonize: bool,
 ) -> Result<(), Error> {
     let config_filename = config_path.to_str().context("Failed to parse VM config path")?;
@@ -89,16 +94,28 @@
 
     if daemonize {
         // Pass the VM reference back to Virt Manager and have it hold it in the background.
-        virt_manager.debugHoldVmRef(&*vm).context("Failed to pass VM to Virt Manager")
+        virt_manager.debugHoldVmRef(&vm).context("Failed to pass VM to Virt Manager")
     } else {
-        // Wait until the VM dies. If we just returned immediately then the IVirtualMachine Binder
-        // object would be dropped and the VM would be killed.
-        wait_for_death(&mut vm.as_binder())?;
-        println!("VM died");
-        Ok(())
+        // Wait until the VM or VirtManager dies. If we just returned immediately then the
+        // IVirtualMachine Binder object would be dropped and the VM would be killed.
+        wait_for_vm(vm)
     }
 }
 
+/// Wait until the given VM or the VirtManager itself dies.
+fn wait_for_vm(vm: Strong<dyn IVirtualMachine>) -> Result<(), Error> {
+    let dead = AtomicFlag::default();
+    let callback =
+        BnVirtualMachineCallback::new_binder(VirtualMachineCallback { dead: dead.clone() });
+    vm.registerCallback(&callback)?;
+    let death_recipient = wait_for_death(&mut vm.as_binder(), dead.clone())?;
+    dead.wait();
+    // Ensure that death_recipient isn't dropped before we wait on the flag, as it is removed
+    // from the Binder when it's dropped.
+    drop(death_recipient);
+    Ok(())
+}
+
 /// Retrieve reference to a previously daemonized VM and stop it.
 fn command_stop(virt_manager: Strong<dyn IVirtManager>, cid: u32) -> Result<(), Error> {
     virt_manager
@@ -115,18 +132,31 @@
     Ok(())
 }
 
-/// Block until the given Binder object dies.
-fn wait_for_death(binder: &mut impl IBinder) -> Result<(), Error> {
-    let dead = AtomicFlag::default();
-    let mut death_recipient = {
-        let dead = dead.clone();
-        DeathRecipient::new(move || {
-            dead.raise();
-        })
-    };
+/// Raise the given flag when the given Binder object dies.
+///
+/// If the returned DeathRecipient is dropped then this will no longer do anything.
+fn wait_for_death(binder: &mut impl IBinder, dead: AtomicFlag) -> Result<DeathRecipient, Error> {
+    let mut death_recipient = DeathRecipient::new(move || {
+        println!("VirtManager died");
+        dead.raise();
+    });
     binder.link_to_death(&mut death_recipient)?;
-    dead.wait();
-    Ok(())
+    Ok(death_recipient)
+}
+
+#[derive(Debug)]
+struct VirtualMachineCallback {
+    dead: AtomicFlag,
+}
+
+impl Interface for VirtualMachineCallback {}
+
+impl IVirtualMachineCallback for VirtualMachineCallback {
+    fn onDied(&self, _cid: i32) -> BinderResult<()> {
+        println!("VM died");
+        self.dead.raise();
+        Ok(())
+    }
 }
 
 /// Safely duplicate the standard output file descriptor.