Merge "Revert "Enable pKVM before running tests""
diff --git a/apex/Android.bp b/apex/Android.bp
index 0e2d2d4..c568ae2 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -50,6 +50,9 @@
     java_libs: [
         "android.system.virtualmachine",
     ],
+    jni_libs: [
+        "libvirtualmachine_jni",
+    ],
     apps: [
         "android.system.virtualmachine.res",
     ],
diff --git a/apkdmverity/Android.bp b/apkdmverity/Android.bp
index df46324..e6b1ca9 100644
--- a/apkdmverity/Android.bp
+++ b/apkdmverity/Android.bp
@@ -17,6 +17,7 @@
         "liblibc",
         "libnix",
         "libnum_traits",
+        "librustutils",
         "libscopeguard",
         "libuuid",
     ],
@@ -31,6 +32,8 @@
 rust_binary {
     name: "apkdmverity",
     defaults: ["apkdmverity.defaults"],
+    init_rc: ["apkdmverity.rc"],
+    bootstrap: true,
 }
 
 rust_test {
diff --git a/apkdmverity/apkdmverity.rc b/apkdmverity/apkdmverity.rc
new file mode 100644
index 0000000..c6ef52b
--- /dev/null
+++ b/apkdmverity/apkdmverity.rc
@@ -0,0 +1,3 @@
+service apkdmverity /system/bin/apkdmverity /dev/block/by-name/microdroid-apk /dev/block/by-name/microdroid-apk-idsig microdroid-apk
+    disabled
+    oneshot
diff --git a/apkdmverity/src/main.rs b/apkdmverity/src/main.rs
index 9d1ef1c..cabeb35 100644
--- a/apkdmverity/src/main.rs
+++ b/apkdmverity/src/main.rs
@@ -28,6 +28,7 @@
 use anyhow::{bail, Context, Result};
 use clap::{App, Arg};
 use idsig::{HashAlgorithm, V4Signature};
+use rustutils::system_properties;
 use std::fmt::Debug;
 use std::fs;
 use std::fs::File;
@@ -61,7 +62,13 @@
     let apk = matches.value_of("apk").unwrap();
     let idsig = matches.value_of("idsig").unwrap();
     let name = matches.value_of("name").unwrap();
-    let ret = enable_verity(apk, idsig, name)?;
+    let roothash = if let Ok(val) = system_properties::read("microdroid_manager.apk_root_hash") {
+        Some(util::parse_hexstring(&val)?)
+    } else {
+        // This failure is not an error. We will use the roothash read from the idsig file.
+        None
+    };
+    let ret = enable_verity(apk, idsig, name, roothash.as_deref())?;
     if matches.is_present("verbose") {
         println!(
             "data_device: {:?}, hash_device: {:?}, mapper_device: {:?}",
@@ -80,7 +87,12 @@
 const BLOCK_SIZE: u64 = 4096;
 
 // Makes a dm-verity block device out of `apk` and its accompanying `idsig` files.
-fn enable_verity<P: AsRef<Path> + Debug>(apk: P, idsig: P, name: &str) -> Result<VerityResult> {
+fn enable_verity<P: AsRef<Path> + Debug>(
+    apk: P,
+    idsig: P,
+    name: &str,
+    roothash: Option<&[u8]>,
+) -> Result<VerityResult> {
     // Attach the apk file to a loop device if the apk file is a regular file. If not (i.e. block
     // device), we only need to get the size and use the block device as it is.
     let (data_device, apk_size) = if fs::metadata(&apk)?.file_type().is_block_device() {
@@ -108,7 +120,11 @@
     let target = dm::DmVerityTargetBuilder::default()
         .data_device(&data_device, apk_size)
         .hash_device(&hash_device)
-        .root_digest(&sig.hashing_info.raw_root_hash)
+        .root_digest(if let Some(roothash) = roothash {
+            roothash
+        } else {
+            &sig.hashing_info.raw_root_hash
+        })
         .hash_algorithm(match sig.hashing_info.hash_algorithm {
             HashAlgorithm::SHA256 => dm::DmVerityHashAlgorithm::SHA256,
         })
@@ -167,6 +183,16 @@
     }
 
     fn run_test(apk: &[u8], idsig: &[u8], name: &str, check: fn(TestContext)) {
+        run_test_with_hash(apk, idsig, name, None, check);
+    }
+
+    fn run_test_with_hash(
+        apk: &[u8],
+        idsig: &[u8],
+        name: &str,
+        roothash: Option<&[u8]>,
+        check: fn(TestContext),
+    ) {
         if should_skip() {
             return;
         }
@@ -174,7 +200,7 @@
         let (apk_path, idsig_path) = prepare_inputs(test_dir.path(), apk, idsig);
 
         // Run the program and register clean-ups.
-        let ret = enable_verity(&apk_path, &idsig_path, name).unwrap();
+        let ret = enable_verity(&apk_path, &idsig_path, name, roothash).unwrap();
         let ret = scopeguard::guard(ret, |ret| {
             loopdevice::detach(ret.data_device).unwrap();
             loopdevice::detach(ret.hash_device).unwrap();
@@ -312,7 +338,8 @@
 
         let name = "loop_as_input";
         // Run the program WITH the loop devices, not the regular files.
-        let ret = enable_verity(apk_loop_device.deref(), idsig_loop_device.deref(), name).unwrap();
+        let ret =
+            enable_verity(apk_loop_device.deref(), idsig_loop_device.deref(), name, None).unwrap();
         let ret = scopeguard::guard(ret, |ret| {
             loopdevice::detach(ret.data_device).unwrap();
             loopdevice::detach(ret.hash_device).unwrap();
@@ -325,4 +352,18 @@
         assert_eq!(verity.len(), original.len()); // fail fast
         assert_eq!(verity.as_slice(), original.as_slice());
     }
+
+    // test with custom roothash
+    #[test]
+    fn correct_custom_roothash() {
+        let apk = include_bytes!("../testdata/test.apk");
+        let idsig = include_bytes!("../testdata/test.apk.idsig");
+        let roothash = V4Signature::from(Cursor::new(&idsig)).unwrap().hashing_info.raw_root_hash;
+        run_test_with_hash(apk.as_ref(), idsig.as_ref(), "correct", Some(&roothash), |ctx| {
+            let verity = fs::read(&ctx.result.mapper_device).unwrap();
+            let original = fs::read(&ctx.result.data_device).unwrap();
+            assert_eq!(verity.len(), original.len()); // fail fast
+            assert_eq!(verity.as_slice(), original.as_slice());
+        });
+    }
 }
diff --git a/apkdmverity/src/util.rs b/apkdmverity/src/util.rs
index d2bc799..913f827 100644
--- a/apkdmverity/src/util.rs
+++ b/apkdmverity/src/util.rs
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-use anyhow::{bail, Result};
+use anyhow::{anyhow, bail, Result};
 use nix::sys::stat::FileStat;
 use std::fs::File;
 use std::os::unix::fs::FileTypeExt;
@@ -42,6 +42,19 @@
     s.iter().map(|byte| format!("{:02x}", byte)).reduce(|i, j| i + &j).unwrap_or_default()
 }
 
+/// Parses a hexadecimal string into a byte array
+pub fn parse_hexstring(s: &str) -> Result<Vec<u8>> {
+    let len = s.len();
+    if len % 2 != 0 {
+        bail!("length {} is not even", len)
+    } else {
+        (0..len)
+            .step_by(2)
+            .map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|e| anyhow!(e)))
+            .collect()
+    }
+}
+
 /// fstat that accepts a path rather than FD
 pub fn fstat(p: &Path) -> Result<FileStat> {
     let f = File::open(p)?;
diff --git a/authfs/aidl/com/android/virt/fs/IVirtFdService.aidl b/authfs/aidl/com/android/virt/fs/IVirtFdService.aidl
index d3c0979..a565a6f 100644
--- a/authfs/aidl/com/android/virt/fs/IVirtFdService.aidl
+++ b/authfs/aidl/com/android/virt/fs/IVirtFdService.aidl
@@ -28,6 +28,9 @@
      */
     const int ERROR_IO = 2;
 
+    /** Error when the file is too large to handle correctly. */
+    const int ERROR_FILE_TOO_LARGE = 3;
+
     /** Maximum content size that the service allows the client to request. */
     const int MAX_REQUESTING_DATA = 16384;
 
@@ -54,4 +57,6 @@
 
     /** Resizes the file backed by the given file ID to the new size. */
     void resize(int id, long size);
+
+    long getFileSize(int id);
 }
diff --git a/authfs/aidl/com/android/virt/fs/InputFdAnnotation.aidl b/authfs/aidl/com/android/virt/fs/InputFdAnnotation.aidl
index dafb137..3534a77 100644
--- a/authfs/aidl/com/android/virt/fs/InputFdAnnotation.aidl
+++ b/authfs/aidl/com/android/virt/fs/InputFdAnnotation.aidl
@@ -23,7 +23,4 @@
      * number used in the backend server.
      */
     int fd;
-
-    /** The actual file size in bytes of the backing file to be read. */
-    long fileSize;
 }
diff --git a/authfs/fd_server/src/main.rs b/authfs/fd_server/src/main.rs
index d63fe93..7e551a3 100644
--- a/authfs/fd_server/src/main.rs
+++ b/authfs/fd_server/src/main.rs
@@ -14,19 +14,19 @@
  * limitations under the License.
  */
 
-//! This program is a constrained file/FD server to serve file requests through a remote[1] binder
+//! This program is a constrained file/FD server to serve file requests through a remote binder
 //! service. The file server is not designed to serve arbitrary file paths in the filesystem. On
 //! the contrary, the server should be configured to start with already opened FDs, and serve the
 //! client's request against the FDs
 //!
 //! For example, `exec 9</path/to/file fd_server --ro-fds 9` starts the binder service. A client
 //! client can then request the content of file 9 by offset and size.
-//!
-//! [1] Since the remote binder is not ready, this currently implementation uses local binder
-//!     first.
 
 mod fsverity;
 
+use anyhow::{bail, Result};
+use binder::unstable_api::AsNative;
+use log::{debug, error};
 use std::cmp::min;
 use std::collections::BTreeMap;
 use std::convert::TryInto;
@@ -36,19 +36,14 @@
 use std::os::unix::fs::FileExt;
 use std::os::unix::io::{AsRawFd, FromRawFd};
 
-use anyhow::{bail, Context, Result};
-use binder::unstable_api::AsNative;
-use log::{debug, error};
-
 use authfs_aidl_interface::aidl::com::android::virt::fs::IVirtFdService::{
-    BnVirtFdService, IVirtFdService, ERROR_IO, ERROR_UNKNOWN_FD, MAX_REQUESTING_DATA,
+    BnVirtFdService, IVirtFdService, ERROR_FILE_TOO_LARGE, ERROR_IO, ERROR_UNKNOWN_FD,
+    MAX_REQUESTING_DATA,
 };
 use authfs_aidl_interface::binder::{
-    add_service, BinderFeatures, ExceptionCode, Interface, ProcessState, Result as BinderResult,
-    Status, StatusCode, Strong,
+    BinderFeatures, ExceptionCode, Interface, Result as BinderResult, Status, StatusCode, Strong,
 };
 
-const SERVICE_NAME: &str = "authfs_fd_server";
 const RPC_SERVICE_PORT: u32 = 3264; // TODO: support dynamic port for multiple fd_server instances
 
 fn new_binder_exception<T: AsRef<str>>(exception: ExceptionCode, message: T) -> Status {
@@ -226,6 +221,30 @@
             }
         }
     }
+
+    fn getFileSize(&self, id: i32) -> BinderResult<i64> {
+        match &self.get_file_config(id)? {
+            FdConfig::Readonly { file, .. } => {
+                let size = file
+                    .metadata()
+                    .map_err(|e| {
+                        error!("getFileSize error: {}", e);
+                        Status::from(ERROR_IO)
+                    })?
+                    .len();
+                Ok(size.try_into().map_err(|e| {
+                    error!("getFileSize: File too large: {}", e);
+                    Status::from(ERROR_FILE_TOO_LARGE)
+                })?)
+            }
+            FdConfig::ReadWrite(_file) => {
+                // Content and metadata of a writable file needs to be tracked by authfs, since
+                // fd_server isn't considered trusted. So there is no point to support getFileSize
+                // for a writable file.
+                Err(new_binder_exception(ExceptionCode::UNSUPPORTED_OPERATION, "Unsupported"))
+            }
+        }
+    }
 }
 
 fn read_into_buf(file: &File, max_size: usize, offset: u64) -> io::Result<Vec<u8>> {
@@ -277,7 +296,7 @@
     Ok((fd, FdConfig::ReadWrite(file)))
 }
 
-fn parse_args() -> Result<(bool, BTreeMap<i32, FdConfig>)> {
+fn parse_args() -> Result<BTreeMap<i32, FdConfig>> {
     #[rustfmt::skip]
     let matches = clap::App::new("fd_server")
         .arg(clap::Arg::with_name("ro-fds")
@@ -288,8 +307,6 @@
              .long("rw-fds")
              .multiple(true)
              .number_of_values(1))
-        .arg(clap::Arg::with_name("rpc-binder")
-             .long("rpc-binder"))
         .get_matches();
 
     let mut fd_pool = BTreeMap::new();
@@ -306,8 +323,7 @@
         }
     }
 
-    let rpc_binder = matches.is_present("rpc-binder");
-    Ok((rpc_binder, fd_pool))
+    Ok(fd_pool)
 }
 
 fn main() -> Result<()> {
@@ -315,32 +331,22 @@
         android_logger::Config::default().with_tag("fd_server").with_min_level(log::Level::Debug),
     );
 
-    let (rpc_binder, fd_pool) = parse_args()?;
+    let fd_pool = parse_args()?;
 
-    if rpc_binder {
-        let mut service = FdService::new_binder(fd_pool).as_binder();
-        debug!("fd_server is starting as a rpc service.");
-        // SAFETY: Service ownership is transferring to the server and won't be valid afterward.
-        // Plus the binder objects are threadsafe.
-        let retval = unsafe {
-            binder_rpc_unstable_bindgen::RunRpcServer(
-                service.as_native_mut() as *mut binder_rpc_unstable_bindgen::AIBinder,
-                RPC_SERVICE_PORT,
-            )
-        };
-        if retval {
-            debug!("RPC server has shut down gracefully");
-            Ok(())
-        } else {
-            bail!("Premature termination of RPC server");
-        }
+    let mut service = FdService::new_binder(fd_pool).as_binder();
+    debug!("fd_server is starting as a rpc service.");
+    // SAFETY: Service ownership is transferring to the server and won't be valid afterward.
+    // Plus the binder objects are threadsafe.
+    let retval = unsafe {
+        binder_rpc_unstable_bindgen::RunRpcServer(
+            service.as_native_mut() as *mut binder_rpc_unstable_bindgen::AIBinder,
+            RPC_SERVICE_PORT,
+        )
+    };
+    if retval {
+        debug!("RPC server has shut down gracefully");
+        Ok(())
     } else {
-        ProcessState::start_thread_pool();
-        let service = FdService::new_binder(fd_pool).as_binder();
-        add_service(SERVICE_NAME, service)
-            .with_context(|| format!("Failed to register service {}", SERVICE_NAME))?;
-        debug!("fd_server is running as a local service.");
-        ProcessState::join_thread_pool();
-        bail!("Unexpected exit after join_thread_pool")
+        bail!("Premature termination of RPC server");
     }
 }
diff --git a/authfs/service/src/authfs.rs b/authfs/service/src/authfs.rs
index 7a466d3..f41a3a6 100644
--- a/authfs/service/src/authfs.rs
+++ b/authfs/service/src/authfs.rs
@@ -129,7 +129,7 @@
         // TODO(b/185178698): Many input files need to be signed and verified.
         // or can we use debug cert for now, which is better than nothing?
         args.push(OsString::from("--remote-ro-file-unverified"));
-        args.push(OsString::from(format!("{}:{}:{}", conf.fd, conf.fd, conf.fileSize)));
+        args.push(OsString::from(format!("{}:{}", conf.fd, conf.fd)));
     }
     for conf in out_fds {
         args.push(OsString::from("--remote-new-rw-file"));
diff --git a/authfs/src/file.rs b/authfs/src/file.rs
index 703eddb..947b59f 100644
--- a/authfs/src/file.rs
+++ b/authfs/src/file.rs
@@ -10,7 +10,7 @@
 
 use crate::common::CHUNK_SIZE;
 use authfs_aidl_interface::aidl::com::android::virt::fs::IVirtFdService::IVirtFdService;
-use authfs_aidl_interface::binder::{get_interface, Strong};
+use authfs_aidl_interface::binder::Strong;
 
 pub type VirtFdService = Strong<dyn IVirtFdService>;
 
@@ -18,17 +18,7 @@
 
 pub const RPC_SERVICE_PORT: u32 = 3264;
 
-fn get_local_binder() -> io::Result<VirtFdService> {
-    let service_name = "authfs_fd_server";
-    get_interface(service_name).map_err(|e| {
-        io::Error::new(
-            io::ErrorKind::AddrNotAvailable,
-            format!("Cannot reach authfs_fd_server binder service: {}", e),
-        )
-    })
-}
-
-fn get_rpc_binder(cid: u32) -> io::Result<VirtFdService> {
+pub fn get_rpc_binder_service(cid: u32) -> io::Result<VirtFdService> {
     // SAFETY: AIBinder returned by RpcClient has correct reference count, and the ownership can be
     // safely taken by new_spibinder.
     let ibinder = unsafe {
@@ -46,14 +36,6 @@
     }
 }
 
-pub fn get_binder_service(cid: Option<u32>) -> io::Result<VirtFdService> {
-    if let Some(cid) = cid {
-        get_rpc_binder(cid)
-    } else {
-        get_local_binder()
-    }
-}
-
 /// A trait for reading data by chunks. Chunks can be read by specifying the chunk index. Only the
 /// last chunk may have incomplete chunk size.
 pub trait ReadByChunk {
diff --git a/authfs/src/main.rs b/authfs/src/main.rs
index 32ea3de..c85d801 100644
--- a/authfs/src/main.rs
+++ b/authfs/src/main.rs
@@ -29,6 +29,7 @@
 
 use anyhow::{bail, Context, Result};
 use std::collections::BTreeMap;
+use std::convert::TryInto;
 use std::fs::File;
 use std::io::Read;
 use std::path::{Path, PathBuf};
@@ -54,7 +55,7 @@
 
     /// CID of the VM where the service runs.
     #[structopt(long)]
-    cid: Option<u32>,
+    cid: u32,
 
     /// Extra options to FUSE
     #[structopt(short = "o")]
@@ -62,16 +63,15 @@
 
     /// A read-only remote file with integrity check. Can be multiple.
     ///
-    /// For example, `--remote-verified-file 5:10:1234:/path/to/cert` tells the filesystem to
-    /// associate entry 5 with a remote file 10 of size 1234 bytes, and need to be verified against
-    /// the /path/to/cert.
+    /// For example, `--remote-verified-file 5:10:/path/to/cert` tells the filesystem to associate
+    /// entry 5 with a remote file 10, and need to be verified against the /path/to/cert.
     #[structopt(long, parse(try_from_str = parse_remote_ro_file_option))]
     remote_ro_file: Vec<OptionRemoteRoFile>,
 
     /// A read-only remote file without integrity check. Can be multiple.
     ///
-    /// For example, `--remote-unverified-file 5:10:1234` tells the filesystem to associate entry 5
-    /// with a remote file 10 of size 1234 bytes.
+    /// For example, `--remote-unverified-file 5:10` tells the filesystem to associate entry 5
+    /// with a remote file 10.
     #[structopt(long, parse(try_from_str = parse_remote_ro_file_unverified_option))]
     remote_ro_file_unverified: Vec<OptionRemoteRoFileUnverified>,
 
@@ -109,10 +109,6 @@
     /// ID to refer to the remote file.
     remote_id: i32,
 
-    /// Expected size of the remote file. Necessary for signature check and Merkle tree
-    /// verification.
-    file_size: u64,
-
     /// Certificate to verify the authenticity of the file's fs-verity signature.
     /// TODO(170494765): Implement PKCS#7 signature verification.
     _certificate_path: PathBuf,
@@ -123,9 +119,6 @@
 
     /// ID to refer to the remote file.
     remote_id: i32,
-
-    /// Expected size of the remote file.
-    file_size: u64,
 }
 
 struct OptionRemoteRwFile {
@@ -161,26 +154,24 @@
 
 fn parse_remote_ro_file_option(option: &str) -> Result<OptionRemoteRoFile> {
     let strs: Vec<&str> = option.split(':').collect();
-    if strs.len() != 4 {
+    if strs.len() != 3 {
         bail!("Invalid option: {}", option);
     }
     Ok(OptionRemoteRoFile {
         ino: strs[0].parse::<Inode>()?,
         remote_id: strs[1].parse::<i32>()?,
-        file_size: strs[2].parse::<u64>()?,
-        _certificate_path: PathBuf::from(strs[3]),
+        _certificate_path: PathBuf::from(strs[2]),
     })
 }
 
 fn parse_remote_ro_file_unverified_option(option: &str) -> Result<OptionRemoteRoFileUnverified> {
     let strs: Vec<&str> = option.split(':').collect();
-    if strs.len() != 3 {
+    if strs.len() != 2 {
         bail!("Invalid option: {}", option);
     }
     Ok(OptionRemoteRoFileUnverified {
         ino: strs[0].parse::<Inode>()?,
         remote_id: strs[1].parse::<i32>()?,
-        file_size: strs[2].parse::<u64>()?,
     })
 }
 
@@ -284,7 +275,7 @@
     let mut file_pool = BTreeMap::new();
 
     if args.has_remote_files() {
-        let service = file::get_binder_service(args.cid)?;
+        let service = file::get_rpc_binder_service(args.cid)?;
 
         for config in &args.remote_ro_file {
             file_pool.insert(
@@ -292,7 +283,7 @@
                 new_config_remote_verified_file(
                     service.clone(),
                     config.remote_id,
-                    config.file_size,
+                    service.getFileSize(config.remote_id)?.try_into()?,
                 )?,
             );
         }
@@ -303,7 +294,7 @@
                 new_config_remote_unverified_file(
                     service.clone(),
                     config.remote_id,
-                    config.file_size,
+                    service.getFileSize(config.remote_id)?.try_into()?,
                 )?,
             );
         }
diff --git a/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java b/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
index 6e1c890..1b4fa4a 100644
--- a/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
+++ b/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
@@ -151,11 +151,10 @@
         // Setup
         runFdServerOnAndroid(
                 "3<input.4m 4<input.4m.merkle_dump 5<input.4m.fsv_sig 6<input.4m",
-                "--ro-fds 3:4:5 --ro-fds 6 --rpc-binder");
+                "--ro-fds 3:4:5 --ro-fds 6");
 
         runAuthFsOnMicrodroid(
-                "--remote-ro-file-unverified 10:6:4194304 --remote-ro-file 11:3:4194304:cert.der"
-                        + " --cid 2");
+                "--remote-ro-file-unverified 10:6 --remote-ro-file 11:3:cert.der --cid 2");
 
         // Action
         String actualHashUnverified4m = computeFileHashOnMicrodroid(MOUNT_DIR + "/10");
@@ -177,9 +176,9 @@
         runFdServerOnAndroid(
                 "3<input.4k 4<input.4k.merkle_dump 5<input.4k.fsv_sig"
                         + " 6<input.4k1 7<input.4k1.merkle_dump 8<input.4k1.fsv_sig",
-                "--ro-fds 3:4:5 --ro-fds 6:7:8 --rpc-binder");
+                "--ro-fds 3:4:5 --ro-fds 6:7:8");
         runAuthFsOnMicrodroid(
-                "--remote-ro-file 10:3:4096:cert.der --remote-ro-file 11:6:4097:cert.der --cid 2");
+                "--remote-ro-file 10:3:cert.der --remote-ro-file 11:6:cert.der --cid 2");
 
         // Action
         String actualHash4k = computeFileHashOnMicrodroid(MOUNT_DIR + "/10");
@@ -198,9 +197,8 @@
             throws DeviceNotAvailableException, InterruptedException {
         // Setup
         runFdServerOnAndroid(
-                "3<input.4m 4<input.4m.merkle_dump.bad 5<input.4m.fsv_sig",
-                "--ro-fds 3:4:5 --rpc-binder");
-        runAuthFsOnMicrodroid("--remote-ro-file 10:3:4096:cert.der --cid 2");
+                "3<input.4m 4<input.4m.merkle_dump.bad 5<input.4m.fsv_sig", "--ro-fds 3:4:5");
+        runAuthFsOnMicrodroid("--remote-ro-file 10:3:cert.der --cid 2");
 
         // Verify
         assertFalse(copyFileOnMicrodroid(MOUNT_DIR + "/10", "/dev/null"));
@@ -210,7 +208,7 @@
     public void testWriteThroughCorrectly()
             throws DeviceNotAvailableException, InterruptedException {
         // Setup
-        runFdServerOnAndroid("3<>output", "--rw-fds 3 --rpc-binder");
+        runFdServerOnAndroid("3<>output", "--rw-fds 3");
         runAuthFsOnMicrodroid("--remote-new-rw-file 20:3 --cid 2");
 
         // Action
@@ -228,7 +226,7 @@
     public void testWriteFailedIfDetectsTampering()
             throws DeviceNotAvailableException, InterruptedException {
         // Setup
-        runFdServerOnAndroid("3<>output", "--rw-fds 3 --rpc-binder");
+        runFdServerOnAndroid("3<>output", "--rw-fds 3");
         runAuthFsOnMicrodroid("--remote-new-rw-file 20:3 --cid 2");
 
         String srcPath = "/system/bin/linker64";
@@ -259,7 +257,7 @@
     @Test
     public void testFileResize() throws DeviceNotAvailableException, InterruptedException {
         // Setup
-        runFdServerOnAndroid("3<>output", "--rw-fds 3 --rpc-binder");
+        runFdServerOnAndroid("3<>output", "--rw-fds 3");
         runAuthFsOnMicrodroid("--remote-new-rw-file 20:3 --cid 2");
         String outputPath = MOUNT_DIR + "/20";
         String backendPath = TEST_DIR + "/output";
diff --git a/compos/Android.bp b/compos/Android.bp
index 6705ea8..2af19c8 100644
--- a/compos/Android.bp
+++ b/compos/Android.bp
@@ -12,6 +12,7 @@
         "libbinder_rpc_unstable_bindgen",
         "libbinder_rs",
         "libclap",
+        "libcompos_common",
         "liblibc",
         "liblog_rust",
         "libminijail_rust",
@@ -40,6 +41,7 @@
         "libbinder_rpc_unstable_bindgen",
         "libbinder_rs",
         "libclap",
+        "libcompos_common",
         "liblibc",
         "liblog_rust",
         "libminijail_rust",
diff --git a/compos/aidl/com/android/compos/InputFdAnnotation.aidl b/compos/aidl/com/android/compos/FdAnnotation.aidl
similarity index 61%
rename from compos/aidl/com/android/compos/InputFdAnnotation.aidl
rename to compos/aidl/com/android/compos/FdAnnotation.aidl
index 44a5591..b910391 100644
--- a/compos/aidl/com/android/compos/InputFdAnnotation.aidl
+++ b/compos/aidl/com/android/compos/FdAnnotation.aidl
@@ -17,13 +17,16 @@
 package com.android.compos;
 
 /** {@hide} */
-parcelable InputFdAnnotation {
+parcelable FdAnnotation {
     /**
-     * File descriptor number to be passed to the program.  This is also the same file descriptor
-     * number used in the backend server.
+     * Input file descriptor numbers to be passed to the program.  This is currently assumed to be
+     * same as the file descriptor number used in the backend server.
      */
-    int fd;
+    int[] input_fds;
 
-    /** The actual file size in bytes of the backing file to be read. */
-    long file_size;
+    /**
+     * Output file descriptor numbers to be passed to the program.  This is currently assumed to be
+     * same as the file descriptor number used in the backend server.
+     */
+    int[] output_fds;
 }
diff --git a/compos/aidl/com/android/compos/ICompOsService.aidl b/compos/aidl/com/android/compos/ICompOsService.aidl
index 3a74940..7904130 100644
--- a/compos/aidl/com/android/compos/ICompOsService.aidl
+++ b/compos/aidl/com/android/compos/ICompOsService.aidl
@@ -18,7 +18,7 @@
 
 import com.android.compos.CompOsKeyData;
 import com.android.compos.CompilationResult;
-import com.android.compos.Metadata;
+import com.android.compos.FdAnnotation;
 
 /** {@hide} */
 interface ICompOsService {
@@ -33,17 +33,17 @@
     void initializeSigningKey(in byte[] keyBlob);
 
     /**
-     * Run dex2oat command with provided args, in a context that may be specified in the Metadata,
+     * Run dex2oat command with provided args, in a context that may be specified in FdAnnotation,
      * e.g. with file descriptors pre-opened. The service is responsible to decide what executables
      * it may run.
      *
      * @param args The command line arguments to run. The 0-th args is normally the program name,
      *             which may not be used by the service. The service may be configured to always use
      *             a fixed executable, or possibly use the 0-th args are the executable lookup hint.
-     * @param metadata Additional information of the execution
+     * @param fd_annotation Additional file descriptor information of the execution
      * @return a CompilationResult
      */
-    CompilationResult compile(in String[] args, in Metadata metadata);
+    CompilationResult compile(in String[] args, in FdAnnotation fd_annotation);
 
     /**
      * Generate a new public/private key pair suitable for signing CompOs output files.
diff --git a/compos/aidl/com/android/compos/OutputFdAnnotation.aidl b/compos/aidl/com/android/compos/OutputFdAnnotation.aidl
deleted file mode 100644
index 95ce425..0000000
--- a/compos/aidl/com/android/compos/OutputFdAnnotation.aidl
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * 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.
- */
-
-package com.android.compos;
-
-/** {@hide} */
-parcelable OutputFdAnnotation {
-    /**
-     * File descriptor number to be passed to the program.  This is currently assumed to be same as
-     * the file descriptor number used in the backend server.
-     */
-    int fd;
-}
diff --git a/compos/apex/Android.bp b/compos/apex/Android.bp
index 5b21802..853b9f4 100644
--- a/compos/apex/Android.bp
+++ b/compos/apex/Android.bp
@@ -39,6 +39,7 @@
 
     binaries: [
         "compos_key_cmd",
+        "compos_verify_key",
         "compsvc",
         "pvm_exec",
     ],
diff --git a/compos/apk/assets/vm_config.json b/compos/apk/assets/vm_config.json
index 3be8a8a..9be60d0 100644
--- a/compos/apk/assets/vm_config.json
+++ b/compos/apk/assets/vm_config.json
@@ -5,10 +5,7 @@
   },
   "task": {
     "type": "executable",
-    "command": "/apex/com.android.compos/bin/compsvc",
-    "args": [
-      "--rpc-binder"
-    ]
+    "command": "/apex/com.android.compos/bin/compsvc"
   },
   "apexes": [
     {
diff --git a/compos/common/Android.bp b/compos/common/Android.bp
new file mode 100644
index 0000000..d8fec81
--- /dev/null
+++ b/compos/common/Android.bp
@@ -0,0 +1,24 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_library {
+    name: "libcompos_common",
+    crate_name: "compos_common",
+    srcs: ["lib.rs"],
+    edition: "2018",
+    rustlibs: [
+        "android.system.virtualizationservice-rust",
+        "compos_aidl_interface-rust",
+        "libanyhow",
+        "libbinder_rpc_unstable_bindgen",
+        "libbinder_rs",
+        "liblog_rust",
+    ],
+    shared_libs: [
+        "libbinder_rpc_unstable",
+    ],
+    apex_available: [
+        "com.android.compos",
+    ],
+}
diff --git a/compos/common/compos_client.rs b/compos/common/compos_client.rs
new file mode 100644
index 0000000..22304f1
--- /dev/null
+++ b/compos/common/compos_client.rs
@@ -0,0 +1,220 @@
+/*
+ * 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.
+ */
+
+//! Support for starting CompOS in a VM and connecting to the service
+
+use crate::{COMPOS_APEX_ROOT, COMPOS_DATA_ROOT, COMPOS_VSOCK_PORT};
+use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
+    IVirtualMachine::IVirtualMachine,
+    IVirtualMachineCallback::{BnVirtualMachineCallback, IVirtualMachineCallback},
+    IVirtualizationService::IVirtualizationService,
+    VirtualMachineAppConfig::VirtualMachineAppConfig,
+    VirtualMachineConfig::VirtualMachineConfig,
+};
+use android_system_virtualizationservice::binder::{
+    wait_for_interface, BinderFeatures, DeathRecipient, IBinder, Interface, ParcelFileDescriptor,
+    Result as BinderResult, Strong,
+};
+use anyhow::{anyhow, bail, Context, Result};
+use binder::{
+    unstable_api::{new_spibinder, AIBinder},
+    FromIBinder,
+};
+use compos_aidl_interface::aidl::com::android::compos::ICompOsService::ICompOsService;
+use std::fs::File;
+use std::path::Path;
+use std::sync::{Arc, Condvar, Mutex};
+use std::thread;
+use std::time::Duration;
+
+/// This owns an instance of the CompOS VM.
+pub struct VmInstance {
+    #[allow(dead_code)] // Keeps the vm alive even if we don`t touch it
+    vm: Strong<dyn IVirtualMachine>,
+    cid: i32,
+}
+
+impl VmInstance {
+    /// Start a new CompOS VM instance using the specified instance image file.
+    pub fn start(instance_image: &Path) -> Result<VmInstance> {
+        let instance_image =
+            File::open(instance_image).context("Failed to open instance image file")?;
+        let instance_fd = ParcelFileDescriptor::new(instance_image);
+
+        let apex_dir = Path::new(COMPOS_APEX_ROOT);
+        let data_dir = Path::new(COMPOS_DATA_ROOT);
+
+        let apk_fd = File::open(apex_dir.join("app/CompOSPayloadApp/CompOSPayloadApp.apk"))
+            .context("Failed to open config APK file")?;
+        let apk_fd = ParcelFileDescriptor::new(apk_fd);
+
+        let idsig_fd = File::open(apex_dir.join("etc/CompOSPayloadApp.apk.idsig"))
+            .context("Failed to open config APK idsig file")?;
+        let idsig_fd = ParcelFileDescriptor::new(idsig_fd);
+
+        // TODO: Send this to stdout instead? Or specify None?
+        let log_fd = File::create(data_dir.join("vm.log")).context("Failed to create log file")?;
+        let log_fd = ParcelFileDescriptor::new(log_fd);
+
+        let config = VirtualMachineConfig::AppConfig(VirtualMachineAppConfig {
+            apk: Some(apk_fd),
+            idsig: Some(idsig_fd),
+            instanceImage: Some(instance_fd),
+            configPath: "assets/vm_config.json".to_owned(),
+            ..Default::default()
+        });
+
+        let service = wait_for_interface::<dyn IVirtualizationService>(
+            "android.system.virtualizationservice",
+        )
+        .context("Failed to find VirtualizationService")?;
+
+        let vm = service.createVm(&config, Some(&log_fd)).context("Failed to create VM")?;
+        let vm_state = Arc::new(VmStateMonitor::default());
+
+        let vm_state_clone = Arc::clone(&vm_state);
+        vm.as_binder().link_to_death(&mut DeathRecipient::new(move || {
+            vm_state_clone.set_died();
+            log::error!("VirtualizationService died");
+        }))?;
+
+        let vm_state_clone = Arc::clone(&vm_state);
+        let callback = BnVirtualMachineCallback::new_binder(
+            VmCallback(vm_state_clone),
+            BinderFeatures::default(),
+        );
+        vm.registerCallback(&callback)?;
+
+        vm.start()?;
+
+        let cid = vm_state.wait_for_start()?;
+
+        // TODO: Use onPayloadReady to avoid this
+        thread::sleep(Duration::from_secs(3));
+
+        Ok(VmInstance { vm, cid })
+    }
+
+    /// Create and return an RPC Binder connection to the Comp OS service in the VM.
+    pub fn get_service(&self) -> Result<Strong<dyn ICompOsService>> {
+        let cid = self.cid as u32;
+        // SAFETY: AIBinder returned by RpcClient has correct reference count, and the ownership
+        // can be safely taken by new_spibinder.
+        let ibinder = unsafe {
+            new_spibinder(
+                binder_rpc_unstable_bindgen::RpcClient(cid, COMPOS_VSOCK_PORT) as *mut AIBinder
+            )
+        }
+        .ok_or_else(|| anyhow!("Failed to connect to CompOS service"))?;
+
+        FromIBinder::try_from(ibinder).context("Connecting to CompOS service")
+    }
+}
+
+#[derive(Debug)]
+struct VmState {
+    has_died: bool,
+    cid: Option<i32>,
+}
+
+impl Default for VmState {
+    fn default() -> Self {
+        Self { has_died: false, cid: None }
+    }
+}
+
+#[derive(Debug)]
+struct VmStateMonitor {
+    mutex: Mutex<VmState>,
+    state_ready: Condvar,
+}
+
+impl Default for VmStateMonitor {
+    fn default() -> Self {
+        Self { mutex: Mutex::new(Default::default()), state_ready: Condvar::new() }
+    }
+}
+
+impl VmStateMonitor {
+    fn set_died(&self) {
+        let mut state = self.mutex.lock().unwrap();
+        state.has_died = true;
+        state.cid = None;
+        drop(state); // Unlock the mutex prior to notifying
+        self.state_ready.notify_all();
+    }
+
+    fn set_started(&self, cid: i32) {
+        let mut state = self.mutex.lock().unwrap();
+        if state.has_died {
+            return;
+        }
+        state.cid = Some(cid);
+        drop(state); // Unlock the mutex prior to notifying
+        self.state_ready.notify_all();
+    }
+
+    fn wait_for_start(&self) -> Result<i32> {
+        let (state, result) = self
+            .state_ready
+            .wait_timeout_while(self.mutex.lock().unwrap(), Duration::from_secs(10), |state| {
+                state.cid.is_none() && !state.has_died
+            })
+            .unwrap();
+        if result.timed_out() {
+            bail!("Timed out waiting for VM")
+        }
+        state.cid.ok_or_else(|| anyhow!("VM died"))
+    }
+}
+
+#[derive(Debug)]
+struct VmCallback(Arc<VmStateMonitor>);
+
+impl Interface for VmCallback {}
+
+impl IVirtualMachineCallback for VmCallback {
+    fn onDied(&self, cid: i32) -> BinderResult<()> {
+        self.0.set_died();
+        log::warn!("VM died, cid = {}", cid);
+        Ok(())
+    }
+
+    fn onPayloadStarted(
+        &self,
+        cid: i32,
+        _stream: Option<&binder::parcel::ParcelFileDescriptor>,
+    ) -> BinderResult<()> {
+        self.0.set_started(cid);
+        // TODO: Use the stream?
+        log::info!("VM payload started, cid = {}", cid);
+        Ok(())
+    }
+
+    fn onPayloadReady(&self, cid: i32) -> BinderResult<()> {
+        // TODO: Use this to trigger vsock connection
+        log::info!("VM payload ready, cid = {}", cid);
+        Ok(())
+    }
+
+    fn onPayloadFinished(&self, cid: i32, exit_code: i32) -> BinderResult<()> {
+        // This should probably never happen in our case, but if it does we means our VM is no
+        // longer running
+        self.0.set_died();
+        log::warn!("VM payload finished, cid = {}, exit code = {}", cid, exit_code);
+        Ok(())
+    }
+}
diff --git a/compos/common/lib.rs b/compos/common/lib.rs
new file mode 100644
index 0000000..081f086
--- /dev/null
+++ b/compos/common/lib.rs
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+
+//! Common items used by CompOS server and/or clients
+
+pub mod compos_client;
+
+/// VSock port that the CompOS server listens on for RPC binder connections. This should be out of
+/// future port range (if happens) that microdroid may reserve for system components.
+pub const COMPOS_VSOCK_PORT: u32 = 6432;
+
+/// The root directory where the CompOS APEX is mounted (read only).
+pub const COMPOS_APEX_ROOT: &str = "/apex/com.android.compos";
+
+/// The root of the  data directory available for private use by the CompOS APEX.
+pub const COMPOS_DATA_ROOT: &str = "/data/misc/apexdata/com.android.compos";
diff --git a/compos/compos_key_cmd/compos_key_cmd.cpp b/compos/compos_key_cmd/compos_key_cmd.cpp
index 874a208..eb11d92 100644
--- a/compos/compos_key_cmd/compos_key_cmd.cpp
+++ b/compos/compos_key_cmd/compos_key_cmd.cpp
@@ -213,7 +213,7 @@
         appConfig.memoryMib = 0; // Use default
 
         LOG(INFO) << "Starting VM";
-        auto status = service->startVm(config, logFd, &mVm);
+        auto status = service->createVm(config, logFd, &mVm);
         if (!status.isOk()) {
             return Error() << status.getDescription();
         }
@@ -224,7 +224,7 @@
             return Error() << status.getDescription();
         }
 
-        LOG(INFO) << "Started VM with cid = " << cid;
+        LOG(INFO) << "Created VM with CID = " << cid;
 
         // We need to use this rather than std::make_shared to make sure the
         // embedded weak_ptr is initialised.
@@ -235,6 +235,12 @@
             return Error() << status.getDescription();
         }
 
+        status = mVm->start();
+        if (!status.isOk()) {
+            return Error() << status.getDescription();
+        }
+        LOG(INFO) << "Started VM";
+
         if (!mCallback->waitForStarted()) {
             return Error() << "VM Payload failed to start";
         }
diff --git a/compos/src/common.rs b/compos/src/common.rs
deleted file mode 100644
index 6cad63a..0000000
--- a/compos/src/common.rs
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * 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.
- */
-
-/// Port to listen. This should be out of future port range (if happens) that microdroid may
-/// reserve for system components.
-pub const VSOCK_PORT: u32 = 6432;
-
-/// Service name of local binder. Used only for debugging purpose.
-pub const SERVICE_NAME: &str = "compsvc";
diff --git a/compos/src/compilation.rs b/compos/src/compilation.rs
index 0199eb5..fec82a6 100644
--- a/compos/src/compilation.rs
+++ b/compos/src/compilation.rs
@@ -27,7 +27,7 @@
     InputFdAnnotation::InputFdAnnotation, OutputFdAnnotation::OutputFdAnnotation,
 };
 use authfs_aidl_interface::binder::{ParcelFileDescriptor, Strong};
-use compos_aidl_interface::aidl::com::android::compos::Metadata::Metadata;
+use compos_aidl_interface::aidl::com::android::compos::FdAnnotation::FdAnnotation;
 
 /// The number that represents the file descriptor number expecting by the task. The number may be
 /// meaningless in the current process.
@@ -50,17 +50,17 @@
     image: ParcelFileDescriptor,
 }
 
-/// Runs the compiler with given flags with file descriptors described in `metadata` retrieved via
-/// `authfs_service`. Returns exit code of the compiler process.
+/// Runs the compiler with given flags with file descriptors described in `fd_annotation` retrieved
+/// via `authfs_service`. Returns exit code of the compiler process.
 pub fn compile(
     compiler_path: &Path,
     compiler_args: &[String],
     authfs_service: Strong<dyn IAuthFsService>,
-    metadata: &Metadata,
+    fd_annotation: &FdAnnotation,
 ) -> Result<CompilerOutput> {
     // Mount authfs (via authfs_service). The authfs instance unmounts once the `authfs` variable
     // is out of scope.
-    let authfs_config = build_authfs_config(metadata);
+    let authfs_config = build_authfs_config(fd_annotation);
     let authfs = authfs_service.mount(&authfs_config)?;
 
     // The task expects to receive FD numbers that match its flags (e.g. --zip-fd=42) prepared
@@ -135,18 +135,18 @@
     })
 }
 
-fn build_authfs_config(metadata: &Metadata) -> AuthFsConfig {
+fn build_authfs_config(fd_annotation: &FdAnnotation) -> AuthFsConfig {
     AuthFsConfig {
         port: 3264, // TODO: support dynamic port
-        inputFdAnnotations: metadata
-            .input_fd_annotations
+        inputFdAnnotations: fd_annotation
+            .input_fds
             .iter()
-            .map(|x| InputFdAnnotation { fd: x.fd, fileSize: x.file_size })
+            .map(|fd| InputFdAnnotation { fd: *fd })
             .collect(),
-        outputFdAnnotations: metadata
-            .output_fd_annotations
+        outputFdAnnotations: fd_annotation
+            .output_fds
             .iter()
-            .map(|x| OutputFdAnnotation { fd: x.fd })
+            .map(|fd| OutputFdAnnotation { fd: *fd })
             .collect(),
     }
 }
diff --git a/compos/src/compos_key_service.rs b/compos/src/compos_key_service.rs
index 92b04f2..4a1566d 100644
--- a/compos/src/compos_key_service.rs
+++ b/compos/src/compos_key_service.rs
@@ -32,15 +32,9 @@
 use ring::signature;
 use scopeguard::ScopeGuard;
 
-/// Keystore2 namespace IDs, used for access control to keys.
-#[derive(Copy, Clone, Debug, PartialEq, Eq)]
-pub enum KeystoreNamespace {
-    /// In the host we re-use the ID assigned to odsign. See system/sepolicy/private/keystore2_key_contexts.
-    // TODO(alanstokes): Remove this.
-    Odsign = 101,
-    /// In a VM we can use the generic ID allocated for payloads. See microdroid's keystore2_key_contexts.
-    VmPayload = 140,
-}
+/// Keystore2 namespace ID, used for access control to keys. In a VM we can use the generic ID
+/// allocated for payloads. See microdroid's keystore2_key_contexts.
+const KEYSTORE_NAMESPACE: i64 = 140;
 
 const KEYSTORE_SERVICE_NAME: &str = "android.system.keystore2.IKeystoreService/default";
 const PURPOSE_SIGN: KeyParameter =
@@ -61,25 +55,21 @@
     KeyParameter { tag: Tag::NO_AUTH_REQUIRED, value: KeyParameterValue::BoolValue(true) };
 
 const BLOB_KEY_DESCRIPTOR: KeyDescriptor =
-    KeyDescriptor { domain: Domain::BLOB, nspace: 0, alias: None, blob: None };
+    KeyDescriptor { domain: Domain::BLOB, nspace: KEYSTORE_NAMESPACE, alias: None, blob: None };
 
 /// An internal service for CompOS key management.
 #[derive(Clone)]
 pub struct CompOsKeyService {
-    namespace: KeystoreNamespace,
     random: SystemRandom,
     security_level: Strong<dyn IKeystoreSecurityLevel>,
 }
 
 impl CompOsKeyService {
-    pub fn new(rpc_binder: bool) -> Result<Self> {
+    pub fn new() -> Result<Self> {
         let keystore_service = wait_for_interface::<dyn IKeystoreService>(KEYSTORE_SERVICE_NAME)
             .context("No Keystore service")?;
 
-        let namespace =
-            if rpc_binder { KeystoreNamespace::VmPayload } else { KeystoreNamespace::Odsign };
         Ok(CompOsKeyService {
-            namespace,
             random: SystemRandom::new(),
             security_level: keystore_service
                 .getSecurityLevel(SecurityLevel::TRUSTED_ENVIRONMENT)
@@ -88,7 +78,7 @@
     }
 
     pub fn do_generate(&self) -> Result<CompOsKeyData> {
-        let key_descriptor = KeyDescriptor { nspace: self.namespace as i64, ..BLOB_KEY_DESCRIPTOR };
+        let key_descriptor = BLOB_KEY_DESCRIPTOR;
         let key_parameters =
             [PURPOSE_SIGN, ALGORITHM, PADDING, DIGEST, KEY_SIZE, EXPONENT, NO_AUTH_REQUIRED];
         let attestation_key = None;
@@ -121,11 +111,7 @@
     }
 
     pub fn do_sign(&self, key_blob: &[u8], data: &[u8]) -> Result<Vec<u8>> {
-        let key_descriptor = KeyDescriptor {
-            nspace: self.namespace as i64,
-            blob: Some(key_blob.to_vec()),
-            ..BLOB_KEY_DESCRIPTOR
-        };
+        let key_descriptor = KeyDescriptor { blob: Some(key_blob.to_vec()), ..BLOB_KEY_DESCRIPTOR };
         let operation_parameters = [PURPOSE_SIGN, ALGORITHM, PADDING, DIGEST];
         let forced = false;
 
diff --git a/compos/src/compsvc.rs b/compos/src/compsvc.rs
index 4a19030..55d9d64 100644
--- a/compos/src/compsvc.rs
+++ b/compos/src/compsvc.rs
@@ -32,8 +32,8 @@
 use compos_aidl_interface::aidl::com::android::compos::{
     CompOsKeyData::CompOsKeyData,
     CompilationResult::CompilationResult,
+    FdAnnotation::FdAnnotation,
     ICompOsService::{BnCompOsService, ICompOsService},
-    Metadata::Metadata,
 };
 use compos_aidl_interface::binder::{
     BinderFeatures, ExceptionCode, Interface, Result as BinderResult, Status, Strong,
@@ -43,10 +43,10 @@
 const DEX2OAT_PATH: &str = "/apex/com.android.art/bin/dex2oat64";
 
 /// Constructs a binder object that implements ICompOsService.
-pub fn new_binder(rpc_binder: bool) -> Result<Strong<dyn ICompOsService>> {
+pub fn new_binder() -> Result<Strong<dyn ICompOsService>> {
     let service = CompOsService {
         dex2oat_path: PathBuf::from(DEX2OAT_PATH),
-        key_service: CompOsKeyService::new(rpc_binder)?,
+        key_service: CompOsKeyService::new()?,
         key_blob: Arc::new(RwLock::new(Vec::new())),
     };
     Ok(BnCompOsService::new_binder(service, BinderFeatures::default()))
@@ -85,14 +85,19 @@
         }
     }
 
-    fn compile(&self, args: &[String], metadata: &Metadata) -> BinderResult<CompilationResult> {
+    fn compile(
+        &self,
+        args: &[String],
+        fd_annotation: &FdAnnotation,
+    ) -> BinderResult<CompilationResult> {
         let authfs_service = get_authfs_service()?;
-        let output = compile(&self.dex2oat_path, args, authfs_service, metadata).map_err(|e| {
-            new_binder_exception(
-                ExceptionCode::SERVICE_SPECIFIC,
-                format!("Compilation failed: {}", e),
-            )
-        })?;
+        let output =
+            compile(&self.dex2oat_path, args, authfs_service, fd_annotation).map_err(|e| {
+                new_binder_exception(
+                    ExceptionCode::SERVICE_SPECIFIC,
+                    format!("Compilation failed: {}", e),
+                )
+            })?;
         match output {
             CompilerOutput::Digests { oat, vdex, image } => {
                 let key = &*self.key_blob.read().unwrap();
diff --git a/compos/src/compsvc_main.rs b/compos/src/compsvc_main.rs
index 6396556..9855b53 100644
--- a/compos/src/compsvc_main.rs
+++ b/compos/src/compsvc_main.rs
@@ -14,65 +14,38 @@
  * limitations under the License.
  */
 
-//! A tool to start a standalone compsvc server, either in the host using Binder or in a VM using
-//! RPC binder over vsock.
+//! A tool to start a standalone compsvc server that serves over RPC binder.
 
-mod common;
 mod compilation;
 mod compos_key_service;
 mod compsvc;
 mod fsverity;
 mod signer;
 
-use crate::common::{SERVICE_NAME, VSOCK_PORT};
-use anyhow::{bail, Context, Result};
+use anyhow::{bail, Result};
 use binder::unstable_api::AsNative;
-use compos_aidl_interface::binder::{add_service, ProcessState};
+use compos_common::COMPOS_VSOCK_PORT;
 use log::debug;
 
-struct Config {
-    rpc_binder: bool,
-}
-
-fn parse_args() -> Result<Config> {
-    #[rustfmt::skip]
-    let matches = clap::App::new("compsvc")
-        .arg(clap::Arg::with_name("rpc_binder")
-             .long("rpc-binder"))
-        .get_matches();
-
-    Ok(Config { rpc_binder: matches.is_present("rpc_binder") })
-}
-
 fn main() -> Result<()> {
     android_logger::init_once(
         android_logger::Config::default().with_tag("compsvc").with_min_level(log::Level::Debug),
     );
 
-    let config = parse_args()?;
-    let mut service = compsvc::new_binder(config.rpc_binder)?.as_binder();
-    if config.rpc_binder {
-        debug!("compsvc is starting as a rpc service.");
-        // SAFETY: Service ownership is transferring to the server and won't be valid afterward.
-        // Plus the binder objects are threadsafe.
-        let retval = unsafe {
-            binder_rpc_unstable_bindgen::RunRpcServer(
-                service.as_native_mut() as *mut binder_rpc_unstable_bindgen::AIBinder,
-                VSOCK_PORT,
-            )
-        };
-        if retval {
-            debug!("RPC server has shut down gracefully");
-            Ok(())
-        } else {
-            bail!("Premature termination of RPC server");
-        }
+    let mut service = compsvc::new_binder()?.as_binder();
+    debug!("compsvc is starting as a rpc service.");
+    // SAFETY: Service ownership is transferring to the server and won't be valid afterward.
+    // Plus the binder objects are threadsafe.
+    let retval = unsafe {
+        binder_rpc_unstable_bindgen::RunRpcServer(
+            service.as_native_mut() as *mut binder_rpc_unstable_bindgen::AIBinder,
+            COMPOS_VSOCK_PORT,
+        )
+    };
+    if retval {
+        debug!("RPC server has shut down gracefully");
+        Ok(())
     } else {
-        ProcessState::start_thread_pool();
-        debug!("compsvc is starting as a local service.");
-        add_service(SERVICE_NAME, service)
-            .with_context(|| format!("Failed to register service {}", SERVICE_NAME))?;
-        ProcessState::join_thread_pool();
-        bail!("Unexpected exit after join_thread_pool")
+        bail!("Premature termination of RPC server");
     }
 }
diff --git a/compos/src/pvm_exec.rs b/compos/src/pvm_exec.rs
index 99eddfc..b6fc729 100644
--- a/compos/src/pvm_exec.rs
+++ b/compos/src/pvm_exec.rs
@@ -29,34 +29,29 @@
 use anyhow::{bail, Context, Result};
 use binder::unstable_api::{new_spibinder, AIBinder};
 use binder::FromIBinder;
+use clap::{value_t, App, Arg};
 use log::{debug, error, warn};
 use minijail::Minijail;
 use nix::fcntl::{fcntl, FcntlArg::F_GETFD};
-use nix::sys::stat::fstat;
 use std::os::unix::io::RawFd;
 use std::path::Path;
 use std::process::exit;
 
 use compos_aidl_interface::aidl::com::android::compos::{
-    ICompOsService::ICompOsService, InputFdAnnotation::InputFdAnnotation, Metadata::Metadata,
-    OutputFdAnnotation::OutputFdAnnotation,
+    FdAnnotation::FdAnnotation, ICompOsService::ICompOsService,
 };
 use compos_aidl_interface::binder::Strong;
-
-mod common;
-use common::{SERVICE_NAME, VSOCK_PORT};
+use compos_common::COMPOS_VSOCK_PORT;
 
 const FD_SERVER_BIN: &str = "/apex/com.android.virt/bin/fd_server";
 
-fn get_local_service() -> Result<Strong<dyn ICompOsService>> {
-    compos_aidl_interface::binder::get_interface(SERVICE_NAME).context("get local binder")
-}
-
 fn get_rpc_binder(cid: u32) -> Result<Strong<dyn ICompOsService>> {
     // SAFETY: AIBinder returned by RpcClient has correct reference count, and the ownership can be
     // safely taken by new_spibinder.
     let ibinder = unsafe {
-        new_spibinder(binder_rpc_unstable_bindgen::RpcClient(cid, VSOCK_PORT) as *mut AIBinder)
+        new_spibinder(
+            binder_rpc_unstable_bindgen::RpcClient(cid, COMPOS_VSOCK_PORT) as *mut AIBinder
+        )
     };
     if let Some(ibinder) = ibinder {
         <dyn ICompOsService>::try_from(ibinder).context("Cannot connect to RPC service")
@@ -65,23 +60,23 @@
     }
 }
 
-fn spawn_fd_server(metadata: &Metadata, debuggable: bool) -> Result<Minijail> {
+fn spawn_fd_server(fd_annotation: &FdAnnotation, debuggable: bool) -> Result<Minijail> {
     let mut inheritable_fds = if debuggable {
         vec![1, 2] // inherit/redirect stdout/stderr for debugging
     } else {
         vec![]
     };
 
-    let mut args = vec![FD_SERVER_BIN.to_string(), "--rpc-binder".to_string()];
-    for metadata in &metadata.input_fd_annotations {
+    let mut args = vec![FD_SERVER_BIN.to_string()];
+    for fd in &fd_annotation.input_fds {
         args.push("--ro-fds".to_string());
-        args.push(metadata.fd.to_string());
-        inheritable_fds.push(metadata.fd);
+        args.push(fd.to_string());
+        inheritable_fds.push(*fd);
     }
-    for metadata in &metadata.output_fd_annotations {
+    for fd in &fd_annotation.output_fds {
         args.push("--rw-fds".to_string());
-        args.push(metadata.fd.to_string());
-        inheritable_fds.push(metadata.fd);
+        args.push(fd.to_string());
+        inheritable_fds.push(*fd);
     }
 
     let jail = Minijail::new()?;
@@ -104,67 +99,49 @@
 
 struct Config {
     args: Vec<String>,
-    metadata: Metadata,
-    cid: Option<u32>,
+    fd_annotation: FdAnnotation,
+    cid: u32,
     debuggable: bool,
 }
 
 fn parse_args() -> Result<Config> {
     #[rustfmt::skip]
-    let matches = clap::App::new("pvm_exec")
-        .arg(clap::Arg::with_name("in-fd")
+    let matches = App::new("pvm_exec")
+        .arg(Arg::with_name("in-fd")
              .long("in-fd")
              .takes_value(true)
              .multiple(true)
              .use_delimiter(true))
-        .arg(clap::Arg::with_name("out-fd")
+        .arg(Arg::with_name("out-fd")
              .long("out-fd")
              .takes_value(true)
              .multiple(true)
              .use_delimiter(true))
-        .arg(clap::Arg::with_name("cid")
+        .arg(Arg::with_name("cid")
              .takes_value(true)
+             .required(true)
              .long("cid"))
-        .arg(clap::Arg::with_name("debug")
+        .arg(Arg::with_name("debug")
              .long("debug"))
-        .arg(clap::Arg::with_name("args")
+        .arg(Arg::with_name("args")
              .last(true)
              .required(true)
              .multiple(true))
         .get_matches();
 
-    let results: Result<Vec<_>> = matches
-        .values_of("in-fd")
-        .unwrap_or_default()
-        .map(|arg| {
-            let fd = parse_arg_fd(arg)?;
-            let file_size = fstat(fd)?.st_size;
-            Ok(InputFdAnnotation { fd, file_size })
-        })
-        .collect();
-    let input_fd_annotations = results?;
+    let results: Result<Vec<_>> =
+        matches.values_of("in-fd").unwrap_or_default().map(parse_arg_fd).collect();
+    let input_fds = results?;
 
-    let results: Result<Vec<_>> = matches
-        .values_of("out-fd")
-        .unwrap_or_default()
-        .map(|arg| {
-            let fd = parse_arg_fd(arg)?;
-            Ok(OutputFdAnnotation { fd })
-        })
-        .collect();
-    let output_fd_annotations = results?;
+    let results: Result<Vec<_>> =
+        matches.values_of("out-fd").unwrap_or_default().map(parse_arg_fd).collect();
+    let output_fds = results?;
 
     let args: Vec<_> = matches.values_of("args").unwrap().map(|s| s.to_string()).collect();
-    let cid =
-        if let Some(arg) = matches.value_of("cid") { Some(arg.parse::<u32>()?) } else { None };
+    let cid = value_t!(matches, "cid", u32)?;
     let debuggable = matches.is_present("debug");
 
-    Ok(Config {
-        args,
-        metadata: Metadata { input_fd_annotations, output_fd_annotations },
-        cid,
-        debuggable,
-    })
+    Ok(Config { args, fd_annotation: FdAnnotation { input_fds, output_fds }, cid, debuggable })
 }
 
 fn main() -> Result<()> {
@@ -175,10 +152,10 @@
     );
 
     // 1. Parse the command line arguments for collect execution data.
-    let Config { args, metadata, cid, debuggable } = parse_args()?;
+    let Config { args, fd_annotation, cid, debuggable } = parse_args()?;
 
     // 2. Spawn and configure a fd_server to serve remote read/write requests.
-    let fd_server_jail = spawn_fd_server(&metadata, debuggable)?;
+    let fd_server_jail = spawn_fd_server(&fd_annotation, debuggable)?;
     let fd_server_lifetime = scopeguard::guard(fd_server_jail, |fd_server_jail| {
         if let Err(e) = fd_server_jail.kill() {
             if !matches!(e, minijail::Error::Killed(_)) {
@@ -188,8 +165,8 @@
     });
 
     // 3. Send the command line args to the remote to execute.
-    let service = if let Some(cid) = cid { get_rpc_binder(cid) } else { get_local_service() }?;
-    let result = service.compile(&args, &metadata).context("Binder call failed")?;
+    let service = get_rpc_binder(cid)?;
+    let result = service.compile(&args, &fd_annotation).context("Binder call failed")?;
 
     // TODO: store/use the signature
     debug!(
diff --git a/compos/verify_key/Android.bp b/compos/verify_key/Android.bp
new file mode 100644
index 0000000..a5892b8
--- /dev/null
+++ b/compos/verify_key/Android.bp
@@ -0,0 +1,22 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_binary {
+    name: "compos_verify_key",
+    srcs: ["verify_key.rs"],
+    edition: "2018",
+    rustlibs: [
+        "compos_aidl_interface-rust",
+        "libandroid_logger",
+        "libanyhow",
+        "libbinder_rs",
+        "libclap",
+        "libcompos_common",
+        "liblog_rust",
+    ],
+    prefer_rlib: true,
+    apex_available: [
+        "com.android.compos",
+    ],
+}
diff --git a/compos/verify_key/verify_key.rs b/compos/verify_key/verify_key.rs
new file mode 100644
index 0000000..2e3d206
--- /dev/null
+++ b/compos/verify_key/verify_key.rs
@@ -0,0 +1,126 @@
+/*
+ * 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.
+ */
+
+//! A tool to verify whether a CompOS instance image and key pair are valid. It starts a CompOS VM
+//! as part of this. The tool is intended to be run by odsign during boot.
+
+use anyhow::{bail, Context, Result};
+use compos_aidl_interface::binder::ProcessState;
+use compos_common::compos_client::VmInstance;
+use compos_common::COMPOS_DATA_ROOT;
+use std::fs::{self, File};
+use std::io::Read;
+use std::path::{Path, PathBuf};
+
+const CURRENT_DIR: &str = "current";
+const PENDING_DIR: &str = "pending";
+const PRIVATE_KEY_BLOB_FILE: &str = "key.blob";
+const PUBLIC_KEY_FILE: &str = "key.pubkey";
+const INSTANCE_IMAGE_FILE: &str = "instance.img";
+
+const MAX_FILE_SIZE_BYTES: u64 = 8 * 1024;
+
+fn main() -> Result<()> {
+    android_logger::init_once(
+        android_logger::Config::default()
+            .with_tag("compos_verify_key")
+            .with_min_level(log::Level::Info),
+    );
+
+    let matches = clap::App::new("compos_verify_key")
+        .arg(
+            clap::Arg::with_name("instance")
+                .long("instance")
+                .takes_value(true)
+                .required(true)
+                .possible_values(&["pending", "current"]),
+        )
+        .get_matches();
+    let do_pending = matches.value_of("instance").unwrap() == "pending";
+
+    let instance_dir: PathBuf =
+        [COMPOS_DATA_ROOT, if do_pending { PENDING_DIR } else { CURRENT_DIR }].iter().collect();
+
+    if !instance_dir.is_dir() {
+        bail!("{} is not a directory", instance_dir.display());
+    }
+
+    // We need to start the thread pool to be able to receive Binder callbacks
+    ProcessState::start_thread_pool();
+
+    let result = verify(&instance_dir).and_then(|_| {
+        if do_pending {
+            // If the pending instance is ok, then it must actually match the current system state,
+            // so we promote it to current.
+            log::info!("Promoting pending to current");
+            promote_to_current(&instance_dir)
+        } else {
+            Ok(())
+        }
+    });
+
+    if result.is_err() {
+        // This is best efforts, and we still want to report the original error as our result
+        log::info!("Removing {}", instance_dir.display());
+        if let Err(e) = fs::remove_dir_all(&instance_dir) {
+            log::warn!("Failed to remove directory: {}", e);
+        }
+    }
+
+    result
+}
+
+fn verify(instance_dir: &Path) -> Result<()> {
+    let blob = instance_dir.join(PRIVATE_KEY_BLOB_FILE);
+    let public_key = instance_dir.join(PUBLIC_KEY_FILE);
+    let instance_image = instance_dir.join(INSTANCE_IMAGE_FILE);
+
+    let blob = read_small_file(blob).context("Failed to read key blob")?;
+    let public_key = read_small_file(public_key).context("Failed to read public key")?;
+
+    let vm_instance = VmInstance::start(&instance_image)?;
+    let service = vm_instance.get_service()?;
+
+    let result = service.verifySigningKey(&blob, &public_key).context("Verifying signing key")?;
+
+    if !result {
+        bail!("Key files are not valid");
+    }
+
+    Ok(())
+}
+
+fn read_small_file(file: PathBuf) -> Result<Vec<u8>> {
+    let mut file = File::open(file)?;
+    if file.metadata()?.len() > MAX_FILE_SIZE_BYTES {
+        bail!("File is too big");
+    }
+    let mut data = vec![];
+    file.read_to_end(&mut data)?;
+    Ok(data)
+}
+
+fn promote_to_current(instance_dir: &Path) -> Result<()> {
+    let current_dir: PathBuf = [COMPOS_DATA_ROOT, CURRENT_DIR].iter().collect();
+
+    // This may fail if the directory doesn't exist - which is fine, we only care about the rename
+    // succeeding.
+    let _ = fs::remove_dir_all(&current_dir);
+
+    fs::rename(&instance_dir, &current_dir)
+        .context("Unable to promote pending instance to current")?;
+    Ok(())
+}
diff --git a/demo/Android.bp b/demo/Android.bp
index 77049de..749ca90 100644
--- a/demo/Android.bp
+++ b/demo/Android.bp
@@ -9,6 +9,7 @@
     static_libs: [
         "androidx-constraintlayout_constraintlayout",
         "androidx.appcompat_appcompat",
+        "com.android.microdroid.testservice-java",
         "com.google.android.material_material",
     ],
     libs: [
@@ -18,4 +19,5 @@
     platform_apis: true,
     use_embedded_native_libs: true,
     v4_signature: true,
+    certificate: "platform",
 }
diff --git a/demo/java/com/android/microdroid/demo/MainActivity.java b/demo/java/com/android/microdroid/demo/MainActivity.java
index 6a46f73..ce21fdf 100644
--- a/demo/java/com/android/microdroid/demo/MainActivity.java
+++ b/demo/java/com/android/microdroid/demo/MainActivity.java
@@ -18,7 +18,9 @@
 
 import android.app.Application;
 import android.os.Bundle;
+import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
 import android.system.virtualmachine.VirtualMachine;
 import android.system.virtualmachine.VirtualMachineCallback;
 import android.system.virtualmachine.VirtualMachineConfig;
@@ -38,6 +40,8 @@
 import androidx.lifecycle.Observer;
 import androidx.lifecycle.ViewModelProvider;
 
+import com.android.microdroid.testservice.ITestService;
+
 import java.io.BufferedReader;
 import java.io.FileInputStream;
 import java.io.IOException;
@@ -45,6 +49,7 @@
 import java.io.InputStreamReader;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
 
 /**
  * This app is to demonstrate the use of APIs in the android.system.virtualmachine library.
@@ -141,7 +146,7 @@
         public void run(boolean debug) {
             // Create a VM and run it.
             // TODO(jiyong): remove the call to idsigPath
-            mExecutorService = Executors.newFixedThreadPool(2);
+            mExecutorService = Executors.newFixedThreadPool(3);
 
             VirtualMachineCallback callback =
                     new VirtualMachineCallback() {
@@ -177,9 +182,55 @@
 
                         @Override
                         public void onPayloadReady(VirtualMachine vm) {
-                            // This check doesn't 100% prevent race condition, but is fine for demo.
-                            if (!mService.isShutdown()) {
-                                mPayloadOutput.postValue("(Payload is ready)");
+                            // This check doesn't 100% prevent race condition or UI hang.
+                            // However, it's fine for demo.
+                            if (mService.isShutdown()) {
+                                return;
+                            }
+                            mPayloadOutput.postValue("(Payload is ready. Testing VM service...)");
+
+                            Future<IBinder> service;
+                            try {
+                                service = vm.connectToVsockServer(ITestService.SERVICE_PORT);
+                            } catch (VirtualMachineException e) {
+                                mPayloadOutput.postValue(
+                                        String.format(
+                                                "(Exception while connecting VM's binder"
+                                                        + " service: %s)",
+                                                e.getMessage()));
+                                return;
+                            }
+
+                            mService.execute(() -> testVMService(service));
+                        }
+
+                        private void testVMService(Future<IBinder> service) {
+                            IBinder binder;
+                            try {
+                                binder = service.get();
+                            } catch (Exception e) {
+                                if (!Thread.interrupted()) {
+                                    mPayloadOutput.postValue(
+                                            String.format(
+                                                    "(VM service connection failed: %s)",
+                                                    e.getMessage()));
+                                }
+                                return;
+                            }
+
+                            try {
+                                ITestService testService = ITestService.Stub.asInterface(binder);
+                                int ret = testService.addInteger(123, 456);
+                                mPayloadOutput.postValue(
+                                        String.format(
+                                                "(VM payload service: %d + %d = %d)",
+                                                123, 456, ret));
+                            } catch (RemoteException e) {
+                                mPayloadOutput.postValue(
+                                        String.format(
+                                                "(Exception while testing VM's binder service:"
+                                                        + " %s)",
+                                                e.getMessage()));
                             }
                         }
 
diff --git a/idsig/Android.bp b/idsig/Android.bp
index c5fc161..3f70a64 100644
--- a/idsig/Android.bp
+++ b/idsig/Android.bp
@@ -15,11 +15,6 @@
         "libnum_traits",
     ],
     proc_macros: ["libnum_derive"],
-    multilib: {
-        lib32: {
-            enabled: false,
-        },
-    },
 }
 
 rust_library {
diff --git a/javalib/AndroidManifest.xml b/javalib/AndroidManifest.xml
index 21857f8..f96b39f 100644
--- a/javalib/AndroidManifest.xml
+++ b/javalib/AndroidManifest.xml
@@ -18,7 +18,7 @@
   package="com.android.virtualmachine.res">
 
   <permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE"
-      android:protectionLevel="normal" />
+      android:protectionLevel="signature" />
 
   <permission android:name="android.permission.DEBUG_VIRTUAL_MACHINE"
       android:protectionLevel="signature" />
diff --git a/javalib/jni/Android.bp b/javalib/jni/Android.bp
new file mode 100644
index 0000000..e141297
--- /dev/null
+++ b/javalib/jni/Android.bp
@@ -0,0 +1,12 @@
+cc_library_shared {
+    name: "libvirtualmachine_jni",
+    srcs: ["android_system_virtualmachine_VirtualMachine.cpp"],
+    apex_available: ["com.android.virt"],
+    shared_libs: [
+        "android.system.virtualizationservice-ndk",
+        "libbinder_ndk",
+        "libbinder_rpc_unstable",
+        "liblog",
+        "libnativehelper",
+    ],
+}
diff --git a/javalib/jni/android_system_virtualmachine_VirtualMachine.cpp b/javalib/jni/android_system_virtualmachine_VirtualMachine.cpp
new file mode 100644
index 0000000..7234dad
--- /dev/null
+++ b/javalib/jni/android_system_virtualmachine_VirtualMachine.cpp
@@ -0,0 +1,88 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "VirtualMachine"
+
+#include <tuple>
+
+#include <log/log.h>
+
+#include <aidl/android/system/virtualizationservice/IVirtualMachine.h>
+#include <android/binder_auto_utils.h>
+#include <android/binder_ibinder_jni.h>
+#include <binder_rpc_unstable.hpp>
+
+#include <jni.h>
+
+JNIEXPORT jobject JNICALL android_system_virtualmachine_VirtualMachine_connectToVsockServer(
+        JNIEnv* env, [[maybe_unused]] jclass clazz, jobject vmBinder, jint port) {
+    using aidl::android::system::virtualizationservice::IVirtualMachine;
+    using ndk::ScopedFileDescriptor;
+    using ndk::SpAIBinder;
+
+    auto vm = IVirtualMachine::fromBinder(SpAIBinder{AIBinder_fromJavaBinder(env, vmBinder)});
+
+    std::tuple args{env, vm.get(), port};
+    using Args = decltype(args);
+
+    auto requestFunc = [](void* param) {
+        auto [env, vm, port] = *static_cast<Args*>(param);
+
+        ScopedFileDescriptor fd;
+        if (auto status = vm->connectVsock(port, &fd); !status.isOk()) {
+            env->ThrowNew(env->FindClass("android/system/virtualmachine/VirtualMachineException"),
+                          ("Failed to connect vsock: " + status.getDescription()).c_str());
+            return -1;
+        }
+
+        // take ownership
+        int ret = fd.get();
+        *fd.getR() = -1;
+
+        return ret;
+    };
+
+    return AIBinder_toJavaBinder(env, RpcPreconnectedClient(requestFunc, &args));
+}
+
+JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* /*reserved*/) {
+    JNIEnv* env;
+    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
+        ALOGE("%s: Failed to get the environment", __FUNCTION__);
+        return JNI_ERR;
+    }
+
+    jclass c = env->FindClass("android/system/virtualmachine/VirtualMachine");
+    if (c == nullptr) {
+        ALOGE("%s: Failed to find class android.system.virtualmachine.VirtualMachine",
+              __FUNCTION__);
+        return JNI_ERR;
+    }
+
+    // Register your class' native methods.
+    static const JNINativeMethod methods[] = {
+            {"nativeConnectToVsockServer", "(Landroid/os/IBinder;I)Landroid/os/IBinder;",
+             reinterpret_cast<void*>(
+                     android_system_virtualmachine_VirtualMachine_connectToVsockServer)},
+    };
+    int rc = env->RegisterNatives(c, methods, sizeof(methods) / sizeof(JNINativeMethod));
+    if (rc != JNI_OK) {
+        ALOGE("%s: Failed to register natives", __FUNCTION__);
+        return rc;
+    }
+
+    return JNI_VERSION_1_6;
+}
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachine.java b/javalib/src/android/system/virtualmachine/VirtualMachine.java
index f4ac467..2da7ecb 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachine.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachine.java
@@ -31,6 +31,7 @@
 import android.system.virtualizationservice.IVirtualizationService;
 import android.system.virtualizationservice.PartitionType;
 import android.system.virtualizationservice.VirtualMachineAppConfig;
+import android.system.virtualizationservice.VirtualMachineState;
 
 import java.io.File;
 import java.io.FileInputStream;
@@ -41,6 +42,9 @@
 import java.nio.file.FileAlreadyExistsException;
 import java.nio.file.Files;
 import java.util.Optional;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
 
 /**
  * A handle to the virtual machine. The virtual machine is local to the app which created the
@@ -109,6 +113,12 @@
     private @Nullable ParcelFileDescriptor mConsoleReader;
     private @Nullable ParcelFileDescriptor mConsoleWriter;
 
+    private final ExecutorService mExecutorService = Executors.newCachedThreadPool();
+
+    static {
+        System.loadLibrary("virtualmachine_jni");
+    }
+
     private VirtualMachine(
             @NonNull Context context, @NonNull String name, @NonNull VirtualMachineConfig config) {
         mPackageName = context.getPackageName();
@@ -224,8 +234,18 @@
     /** Returns the current status of this virtual machine. */
     public @NonNull Status getStatus() throws VirtualMachineException {
         try {
-            if (mVirtualMachine != null && mVirtualMachine.isRunning()) {
-                return Status.RUNNING;
+            if (mVirtualMachine != null) {
+                switch (mVirtualMachine.getState()) {
+                    case VirtualMachineState.NOT_STARTED:
+                        return Status.STOPPED;
+                    case VirtualMachineState.STARTING:
+                    case VirtualMachineState.STARTED:
+                    case VirtualMachineState.READY:
+                    case VirtualMachineState.FINISHED:
+                        return Status.RUNNING;
+                    case VirtualMachineState.DEAD:
+                        return Status.STOPPED;
+                }
             }
         } catch (RemoteException e) {
             throw new VirtualMachineException(e);
@@ -290,7 +310,7 @@
             android.system.virtualizationservice.VirtualMachineConfig vmConfigParcel =
                     android.system.virtualizationservice.VirtualMachineConfig.appConfig(appConfig);
 
-            mVirtualMachine = service.startVm(vmConfigParcel, mConsoleWriter);
+            mVirtualMachine = service.createVm(vmConfigParcel, mConsoleWriter);
             mVirtualMachine.registerCallback(
                     new IVirtualMachineCallback.Stub() {
                         @Override
@@ -341,7 +361,7 @@
                                 }
                             },
                             0);
-
+            mVirtualMachine.start();
         } catch (IOException e) {
             throw new VirtualMachineException(e);
         } catch (RemoteException e) {
@@ -430,6 +450,25 @@
         return oldConfig;
     }
 
+    private static native IBinder nativeConnectToVsockServer(IBinder vmBinder, int port);
+
+    /**
+     * Connects to a VM's RPC server via vsock, and returns a root IBinder object. Guest VMs are
+     * expected to set up vsock servers in their payload. After the host app receives onPayloadReady
+     * callback, the host app can use this method to establish an RPC session to the guest VMs.
+     *
+     * <p>If the connection succeeds, the root IBinder object will be returned via {@link
+     * VirtualMachineCallback.onVsockServerReady()}. If the connection fails, {@link
+     * VirtualMachineCallback.onVsockServerConnectionFailed()} will be called.
+     */
+    public Future<IBinder> connectToVsockServer(int port) throws VirtualMachineException {
+        if (getStatus() != Status.RUNNING) {
+            throw new VirtualMachineException("VM is not running");
+        }
+        return mExecutorService.submit(
+                () -> nativeConnectToVsockServer(mVirtualMachine.asBinder(), port));
+    }
+
     @Override
     public String toString() {
         StringBuilder sb = new StringBuilder();
diff --git a/microdroid/Android.bp b/microdroid/Android.bp
index 52c00b8..e4334cb 100644
--- a/microdroid/Android.bp
+++ b/microdroid/Android.bp
@@ -101,7 +101,10 @@
     multilib: {
         common: {
             deps: [
+                // non-updatable & mandatory apexes
+                "com.android.i18n",
                 "com.android.runtime",
+
                 "microdroid_plat_sepolicy.cil",
                 "microdroid_plat_mapping_file",
             ],
@@ -165,6 +168,7 @@
 
 android_filesystem {
     name: "microdroid_vendor",
+    partition_name: "vendor",
     use_avb: true,
     deps: [
         "android.hardware.security.keymint-service.microdroid",
@@ -194,11 +198,11 @@
     size: "auto",
     default_group: [
         {
-            name: "system",
+            name: "system_a",
             filesystem: ":microdroid",
         },
         {
-            name: "vendor",
+            name: "vendor_a",
             filesystem: ":microdroid_vendor",
         },
     ],
@@ -419,11 +423,6 @@
             rollback_index_location: 1,
             private_key: ":avb_testkey_rsa4096",
         },
-        {
-            name: "boot",
-            rollback_index_location: 2,
-            private_key: ":avb_testkey_rsa4096",
-        },
     ],
 }
 
@@ -433,6 +432,7 @@
     private_key: ":avb_testkey_rsa4096",
     partitions: [
         "microdroid",
+        "microdroid_boot-5.10",
     ],
 }
 
diff --git a/microdroid/fstab.microdroid b/microdroid/fstab.microdroid
index 129718e..f0e70b6 100644
--- a/microdroid/fstab.microdroid
+++ b/microdroid/fstab.microdroid
@@ -1,2 +1,2 @@
-system /system ext4 noatime,ro,errors=panic wait,first_stage_mount,logical
-vendor /vendor ext4 noatime,ro,errors=panic wait,first_stage_mount,logical
+system /system ext4 noatime,ro,errors=panic wait,slotselect,avb=vbmeta_system,first_stage_mount,logical
+vendor /vendor ext4 noatime,ro,errors=panic wait,slotselect,avb=vbmeta,first_stage_mount,logical
diff --git a/microdroid/init.rc b/microdroid/init.rc
index f9cd915..23434bb 100644
--- a/microdroid/init.rc
+++ b/microdroid/init.rc
@@ -17,18 +17,18 @@
 
     start ueventd
 
+    mkdir /mnt/apk 0755 system system
+    # Microdroid_manager starts apkdmverity/zipfuse/apexd
     start microdroid_manager
-    # TODO(b/190343842) verify apexes/apk before mounting them.
 
-    # Exec apexd in the VM mode to avoid unnecessary overhead of normal mode.
-    # (e.g. session management)
-    exec - root system -- /system/bin/apexd --vm
-
+    # Wait for apexd to finish activating APEXes before starting more processes.
+    wait_for_prop apexd.status activated
     perform_apex_config
 
-    exec - root system -- /system/bin/apkdmverity /dev/block/by-name/microdroid-apk /dev/block/by-name/microdroid-apk-idsig microdroid-apk
-    mkdir /mnt/apk 0755 system system
-    start zipfuse
+    # Notify to microdroid_manager that perform_apex_config is done.
+    # Microdroid_manager shouldn't execute payload before this, because app
+    # payloads are not designed to run with bootstrap bionic
+    setprop apex_config.done true
 
 on init
     # Mount binderfs
@@ -169,6 +169,12 @@
     mkdir /data/local 0751 root root
     mkdir /data/local/tmp 0771 shell shell
 
+service apexd-vm /system/bin/apexd --vm
+    user root
+    group system
+    oneshot
+    disabled
+
 service ueventd /system/bin/ueventd
     class core
     critical
diff --git a/microdroid/payload/metadata.proto b/microdroid/payload/metadata.proto
index 0fa0650..4c32dde 100644
--- a/microdroid/payload/metadata.proto
+++ b/microdroid/payload/metadata.proto
@@ -38,11 +38,7 @@
 
   // 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;
+  bytes public_key = 3;
 }
 
 message ApkPayload {
diff --git a/microdroid/uboot-env-x86_64.txt b/microdroid/uboot-env-x86_64.txt
index 1abafa6..0064cac 100644
--- a/microdroid/uboot-env-x86_64.txt
+++ b/microdroid/uboot-env-x86_64.txt
@@ -3,7 +3,7 @@
 # Boot the device following the Android boot procedure
 # `0` is the disk number of os_composite.img
 # `a` and `_a` are the slot index for A/B
-bootcmd=avb init virtio 0 && avb verify _a && boot_android virtio 0 a
+bootcmd=verified_boot_android virtio 0 a
 
 bootdelay=0
 
diff --git a/microdroid/uboot-env.txt b/microdroid/uboot-env.txt
index 585702e..b5f3968 100644
--- a/microdroid/uboot-env.txt
+++ b/microdroid/uboot-env.txt
@@ -3,7 +3,7 @@
 # Boot the device following the Android boot procedure
 # `0` is the disk number of os_composite.img
 # `a` and `_a` are the slot index for A/B
-bootcmd=avb init virtio 0 && avb verify _a && boot_android virtio 0 a
+bootcmd=verified_boot_android virtio 0 a
 
 bootdelay=0
 fdtaddr=0x80000000
diff --git a/microdroid_manager/Android.bp b/microdroid_manager/Android.bp
index 0b1bc33..9957689 100644
--- a/microdroid_manager/Android.bp
+++ b/microdroid_manager/Android.bp
@@ -27,10 +27,12 @@
         "libring",
         "librustutils",
         "libserde",
+        "libserde_cbor",
         "libserde_json",
         "libuuid",
         "libvsock",
         "librand",
+        "libzip",
     ],
     shared_libs: [
         "libbinder_rpc_unstable",
diff --git a/microdroid_manager/src/instance.rs b/microdroid_manager/src/instance.rs
index 3ef3b63..73983a7 100644
--- a/microdroid_manager/src/instance.rs
+++ b/microdroid_manager/src/instance.rs
@@ -37,6 +37,7 @@
 use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
 use ring::aead::{Aad, Algorithm, LessSafeKey, Nonce, UnboundKey, AES_256_GCM};
 use ring::hkdf::{Salt, HKDF_SHA256};
+use serde::{Deserialize, Serialize};
 use std::fs::{File, OpenOptions};
 use std::io::{Read, Seek, SeekFrom, Write};
 use uuid::Uuid;
@@ -104,7 +105,7 @@
     /// Reads the identity data that was written by microdroid manager. The returned data is
     /// plaintext, although it is stored encrypted. In case when the partition for microdroid
     /// manager doesn't exist, which can happen if it's the first boot, `Ok(None)` is returned.
-    pub fn read_microdroid_data(&mut self) -> Result<Option<Box<[u8]>>> {
+    pub fn read_microdroid_data(&mut self) -> Result<Option<MicrodroidData>> {
         let (header, offset) = self.locate_microdroid_header()?;
         if header.is_none() {
             return Ok(None);
@@ -134,14 +135,17 @@
         // Truncate to remove the tag
         data.truncate(plaintext_len);
 
-        Ok(Some(data.into_boxed_slice()))
+        let microdroid_data = serde_cbor::from_slice(data.as_slice())?;
+        Ok(Some(microdroid_data))
     }
 
     /// Writes identity data to the partition for microdroid manager. The partition is appended
     /// if it doesn't exist. The data is stored encrypted.
-    pub fn write_microdroid_data(&mut self, data: &[u8]) -> Result<()> {
+    pub fn write_microdroid_data(&mut self, microdroid_data: &MicrodroidData) -> Result<()> {
         let (header, offset) = self.locate_microdroid_header()?;
 
+        let mut data = serde_cbor::to_vec(microdroid_data)?;
+
         // By encrypting and signing the data, tag will be appended. The tag also becomes part of
         // the encrypted payload which will be written. In addition, a 12-bytes nonce will be
         // prepended (non-encrypted).
@@ -170,7 +174,6 @@
 
         // Then encrypt and sign the data. The non-encrypted input data is copied to a vector
         // because it is encrypted in place, and also the tag is appended.
-        let mut data = data.to_vec();
         get_key().seal_in_place_append_tag(nonce, Aad::from(&header), &mut data)?;
 
         // Persist the encrypted payload data
@@ -307,3 +310,23 @@
 
     ret
 }
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+pub struct MicrodroidData {
+    pub apk_data: ApkData,
+    pub apex_data: Vec<ApexData>,
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+pub struct ApkData {
+    pub root_hash: Box<RootHash>,
+    // TODO(b/199143508) add cert
+}
+
+pub type RootHash = [u8];
+
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+pub struct ApexData {
+    pub name: String,
+    pub pubkey: Vec<u8>,
+}
diff --git a/microdroid_manager/src/ioutil.rs b/microdroid_manager/src/ioutil.rs
index e8732ad..ab82e05 100644
--- a/microdroid_manager/src/ioutil.rs
+++ b/microdroid_manager/src/ioutil.rs
@@ -14,6 +14,7 @@
 
 //! IO utilities
 
+use anyhow::{anyhow, Result};
 use std::fs::File;
 use std::io;
 use std::path::Path;
@@ -23,17 +24,17 @@
 const SLEEP_DURATION: Duration = Duration::from_millis(5);
 
 /// waits for a file with a timeout and returns it
-pub fn wait_for_file<P: AsRef<Path>>(path: P, timeout: Duration) -> io::Result<File> {
+pub fn wait_for_file<P: AsRef<Path>>(path: P, timeout: Duration) -> Result<File> {
     let begin = Instant::now();
     loop {
         match File::open(&path) {
             Ok(file) => return Ok(file),
             Err(error) => {
                 if error.kind() != io::ErrorKind::NotFound {
-                    return Err(error);
+                    return Err(anyhow!(error));
                 }
                 if begin.elapsed() > timeout {
-                    return Err(io::Error::from(io::ErrorKind::NotFound));
+                    return Err(anyhow!(io::Error::from(io::ErrorKind::NotFound)));
                 }
                 thread::sleep(SLEEP_DURATION);
             }
@@ -47,7 +48,7 @@
     use std::io::{Read, Write};
 
     #[test]
-    fn test_wait_for_file() -> io::Result<()> {
+    fn test_wait_for_file() -> Result<()> {
         let test_dir = tempfile::TempDir::new().unwrap();
         let test_file = test_dir.path().join("test.txt");
         thread::spawn(move || -> io::Result<()> {
@@ -69,6 +70,9 @@
         let test_file = test_dir.path().join("test.txt");
         let file = wait_for_file(&test_file, Duration::from_secs(1));
         assert!(file.is_err());
-        assert_eq!(io::ErrorKind::NotFound, file.unwrap_err().kind());
+        assert_eq!(
+            io::ErrorKind::NotFound,
+            file.unwrap_err().root_cause().downcast_ref::<io::Error>().unwrap().kind()
+        );
     }
 }
diff --git a/microdroid_manager/src/main.rs b/microdroid_manager/src/main.rs
index 278a14d..204feab 100644
--- a/microdroid_manager/src/main.rs
+++ b/microdroid_manager/src/main.rs
@@ -16,27 +16,32 @@
 
 mod instance;
 mod ioutil;
-mod metadata;
+mod payload;
 
-use crate::instance::InstanceDisk;
-use anyhow::{anyhow, bail, Context, Result};
+use crate::instance::{ApkData, InstanceDisk, MicrodroidData, RootHash};
+use anyhow::{anyhow, bail, ensure, Context, Result};
 use apkverify::verify;
 use binder::unstable_api::{new_spibinder, AIBinder};
 use binder::{FromIBinder, Strong};
 use idsig::V4Signature;
-use log::{debug, error, info, warn};
+use log::{error, info, warn};
+use microdroid_metadata::{write_metadata, Metadata};
 use microdroid_payload_config::{Task, TaskType, VmPayloadConfig};
 use nix::ioctl_read_bad;
+use payload::{get_apex_data_from_payload, load_metadata, to_metadata};
+use rustutils::system_properties;
 use rustutils::system_properties::PropertyWatcher;
 use std::fs::{self, File, OpenOptions};
 use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd};
 use std::path::Path;
 use std::process::{Command, Stdio};
 use std::str;
-use std::time::Duration;
+use std::time::{Duration, SystemTime};
 use vsock::VsockStream;
 
-use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice::IVirtualMachineService::IVirtualMachineService;
+use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice::IVirtualMachineService::{
+    VM_BINDER_SERVICE_PORT, VM_STREAM_SERVICE_PORT, IVirtualMachineService,
+};
 
 const WAIT_TIMEOUT: Duration = Duration::from_secs(10);
 const DM_MOUNTED_APK_PATH: &str = "/dev/block/mapper/microdroid-apk";
@@ -44,10 +49,7 @@
 /// The CID representing the host VM
 const VMADDR_CID_HOST: u32 = 2;
 
-/// Port number that virtualizationservice listens on connections from the guest VMs for the
-/// VirtualMachineService binder service
-/// Sync with virtualizationservice/src/aidl.rs
-const PORT_VM_BINDER_SERVICE: u32 = 5000;
+const APEX_CONFIG_DONE_PROP: &str = "apex_config.done";
 
 fn get_vms_rpc_binder() -> Result<Strong<dyn IVirtualMachineService>> {
     // SAFETY: AIBinder returned by RpcClient has correct reference count, and the ownership can be
@@ -55,7 +57,7 @@
     let ibinder = unsafe {
         new_spibinder(binder_rpc_unstable_bindgen::RpcClient(
             VMADDR_CID_HOST,
-            PORT_VM_BINDER_SERVICE,
+            VM_BINDER_SERVICE_PORT as u32,
         ) as *mut AIBinder)
     };
     if let Some(ibinder) = ibinder {
@@ -90,27 +92,29 @@
     kernlog::init()?;
     info!("started.");
 
-    let metadata = metadata::load()?;
-
-    if let Err(err) = verify_payloads() {
-        error!("failed to verify payload: {:#?}", err);
-        return Err(err);
-    }
+    let metadata = load_metadata()?;
 
     let mut instance = InstanceDisk::new()?;
-    // TODO(jiyong): the data should have an internal structure
-    if let Some(data) = instance.read_microdroid_data().context("Failed to read identity data")? {
-        debug!("read apk root hash: {}", to_hex_string(&data));
-        //TODO(jiyong) apkdmverity should use this root hash instead of the one read from the idsig
-        //file, if the root hash is found in the instance image.
+    let saved_data = instance.read_microdroid_data().context("Failed to read identity data")?;
+
+    // Verify the payload before using it.
+    let verified_data =
+        verify_payload(&metadata, saved_data.as_ref()).context("Payload verification failed")?;
+    if let Some(saved_data) = saved_data {
+        if saved_data == verified_data {
+            info!("Saved data is verified.");
+        } else {
+            bail!("Detected an update of the payload which isn't supported yet.");
+        }
     } else {
-        let data = get_apk_roothash()?;
-        debug!("write apk root hash: {}", to_hex_string(&data));
-        instance.write_microdroid_data(data.as_ref()).context("Failed to write identity data")?;
+        info!("Saving verified data.");
+        instance.write_microdroid_data(&verified_data).context("Failed to write identity data")?;
     }
 
-    let service = get_vms_rpc_binder().expect("cannot connect to VirtualMachineService");
+    // Before reading a file from the APK, start zipfuse
+    system_properties::write("ctl.start", "zipfuse")?;
 
+    let service = get_vms_rpc_binder().expect("cannot connect to VirtualMachineService");
     if !metadata.payload_config_path.is_empty() {
         let config = load_config(Path::new(&metadata.payload_config_path))?;
 
@@ -119,7 +123,10 @@
             warn!("failed to set ro.vmsecret.keymint: {}", err);
         }
 
+        // Wait until apex config is done. (e.g. linker configuration for apexes)
         // TODO(jooyung): wait until sys.boot_completed?
+        wait_for_apex_config_done()?;
+
         if let Some(main_task) = &config.task {
             exec_task(main_task, &service).map_err(|e| {
                 error!("failed to execute task: {}", e);
@@ -131,20 +138,83 @@
     Ok(())
 }
 
-// TODO(jooyung): v2/v3 full verification can be slow. Consider multithreading.
-fn verify_payloads() -> Result<()> {
-    // We don't verify APEXes since apexd does.
+// Verify payload before executing it. For APK payload, Full verification (which is slow) is done
+// when the root_hash values from the idsig file and the instance disk are different. This function
+// returns the verified root hash (for APK payload) and pubkeys (for APEX payloads) that can be
+// saved to the instance disk.
+fn verify_payload(
+    metadata: &Metadata,
+    saved_data: Option<&MicrodroidData>,
+) -> Result<MicrodroidData> {
+    let start_time = SystemTime::now();
 
-    // should wait APK to be dm-verity mounted by apkdmverity
+    let root_hash = saved_data.map(|d| &d.apk_data.root_hash);
+    let root_hash_from_idsig = get_apk_root_hash_from_idsig()?;
+    let root_hash_trustful = root_hash == Some(&root_hash_from_idsig);
+
+    // If root_hash can be trusted, pass it to apkdmverity so that it uses the passed root_hash
+    // instead of the value read from the idsig file.
+    if root_hash_trustful {
+        let root_hash = to_hex_string(root_hash.unwrap());
+        system_properties::write("microdroid_manager.apk_root_hash", &root_hash)?;
+    }
+
+    // Start apkdmverity and wait for the dm-verify block
+    system_properties::write("ctl.start", "apkdmverity")?;
+
+    // While waiting for apkdmverity to mount APK, gathers APEX pubkeys from payload.
+    let apex_data_from_payload = get_apex_data_from_payload(metadata)?;
+    if let Some(saved_data) = saved_data.map(|d| &d.apex_data) {
+        // For APEX payload, we don't support updating their pubkeys
+        ensure!(saved_data == &apex_data_from_payload, "APEX payloads has changed.");
+        let apex_metadata = to_metadata(&apex_data_from_payload);
+        // Pass metadata(with pubkeys) to apexd so that it uses the passed metadata
+        // instead of the default one (/dev/block/by-name/payload-metadata)
+        OpenOptions::new()
+            .create_new(true)
+            .write(true)
+            .open("/apex/vm-payload-metadata")
+            .context("Failed to open /apex/vm-payload-metadata")
+            .and_then(|f| write_metadata(&apex_metadata, f))?;
+    }
+    // Start apexd to activate APEXes
+    system_properties::write("ctl.start", "apexd-vm")?;
+
     ioutil::wait_for_file(DM_MOUNTED_APK_PATH, WAIT_TIMEOUT)?;
-    verify(DM_MOUNTED_APK_PATH).context(format!("failed to verify {}", DM_MOUNTED_APK_PATH))?;
 
-    info!("payload verification succeeded.");
-    // TODO(jooyung): collect public keys and store them in instance.img
+    // Do the full verification if the root_hash is un-trustful. This requires the full scanning of
+    // the APK file and therefore can be very slow if the APK is large. Note that this step is
+    // taken only when the root_hash is un-trustful which can be either when this is the first boot
+    // of the VM or APK was updated in the host.
+    // TODO(jooyung): consider multithreading to make this faster
+    if !root_hash_trustful {
+        verify(DM_MOUNTED_APK_PATH).context(format!("failed to verify {}", DM_MOUNTED_APK_PATH))?;
+    }
+
+    info!("payload verification successful. took {:#?}", start_time.elapsed().unwrap());
+
+    // At this point, we can ensure that the root_hash from the idsig file is trusted, either by
+    // fully verifying the APK or by comparing it with the saved root_hash.
+    Ok(MicrodroidData {
+        apk_data: ApkData { root_hash: root_hash_from_idsig },
+        apex_data: apex_data_from_payload,
+    })
+}
+
+// Waits until linker config is generated
+fn wait_for_apex_config_done() -> Result<()> {
+    let mut prop = PropertyWatcher::new(APEX_CONFIG_DONE_PROP)?;
+    loop {
+        prop.wait()?;
+        let val = system_properties::read(APEX_CONFIG_DONE_PROP)?;
+        if val == "true" {
+            break;
+        }
+    }
     Ok(())
 }
 
-fn get_apk_roothash() -> Result<Box<[u8]>> {
+fn get_apk_root_hash_from_idsig() -> Result<Box<RootHash>> {
     let mut idsig = File::open("/dev/block/by-name/microdroid-apk-idsig")?;
     let idsig = V4Signature::from(&mut idsig)?;
     Ok(idsig.hashing_info.raw_root_hash)
@@ -183,7 +253,6 @@
 
 fn build_command(task: &Task) -> Result<Command> {
     const VMADDR_CID_HOST: u32 = 2;
-    const PORT_VIRT_SVC: u32 = 3000;
 
     let mut command = match task.type_ {
         TaskType::Executable => {
@@ -198,7 +267,7 @@
         }
     };
 
-    match VsockStream::connect_with_cid_port(VMADDR_CID_HOST, PORT_VIRT_SVC) {
+    match VsockStream::connect_with_cid_port(VMADDR_CID_HOST, VM_STREAM_SERVICE_PORT as u32) {
         Ok(stream) => {
             // SAFETY: the ownership of the underlying file descriptor is transferred from stream
             // to the file object, and then into the Command object. When the command is finished,
diff --git a/microdroid_manager/src/metadata.rs b/microdroid_manager/src/metadata.rs
deleted file mode 100644
index 432a134..0000000
--- a/microdroid_manager/src/metadata.rs
+++ /dev/null
@@ -1,32 +0,0 @@
-// 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 metadata from /dev/block/by-name/payload-metadata
-
-use crate::ioutil;
-
-use anyhow::Result;
-use log::info;
-use microdroid_metadata::{read_metadata, Metadata};
-use std::time::Duration;
-
-const WAIT_TIMEOUT: Duration = Duration::from_secs(10);
-const PAYLOAD_METADATA_PATH: &str = "/dev/block/by-name/payload-metadata";
-
-/// loads payload metadata from /dev/block/by-name/paylaod-metadata
-pub fn load() -> Result<Metadata> {
-    info!("loading payload metadata...");
-    let file = ioutil::wait_for_file(PAYLOAD_METADATA_PATH, WAIT_TIMEOUT)?;
-    read_metadata(file)
-}
diff --git a/microdroid_manager/src/payload.rs b/microdroid_manager/src/payload.rs
new file mode 100644
index 0000000..bf9d9f9
--- /dev/null
+++ b/microdroid_manager/src/payload.rs
@@ -0,0 +1,74 @@
+// 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.
+
+//! Routines for handling payload
+
+use crate::instance::ApexData;
+use crate::ioutil::wait_for_file;
+use anyhow::Result;
+use log::info;
+use microdroid_metadata::{read_metadata, ApexPayload, Metadata};
+use std::fs::File;
+use std::io::Read;
+use std::time::Duration;
+use zip::ZipArchive;
+
+const APEX_PUBKEY_ENTRY: &str = "apex_pubkey";
+const PAYLOAD_METADATA_PATH: &str = "/dev/block/by-name/payload-metadata";
+const WAIT_TIMEOUT: Duration = Duration::from_secs(10);
+
+/// Loads payload metadata from /dev/block/by-name/payload-metadata
+pub fn load_metadata() -> Result<Metadata> {
+    info!("loading payload metadata...");
+    let file = wait_for_file(PAYLOAD_METADATA_PATH, WAIT_TIMEOUT)?;
+    read_metadata(file)
+}
+
+/// Loads (name, pubkey) from payload APEXes
+pub fn get_apex_data_from_payload(metadata: &Metadata) -> Result<Vec<ApexData>> {
+    metadata
+        .apexes
+        .iter()
+        .map(|apex| {
+            let name = apex.name.clone();
+            let partition = format!("/dev/block/by-name/{}", apex.partition_name);
+            let pubkey = get_pubkey_from_apex(&partition)?;
+            Ok(ApexData { name, pubkey })
+        })
+        .collect()
+}
+
+fn get_pubkey_from_apex(path: &str) -> Result<Vec<u8>> {
+    let f = File::open(path)?;
+    let mut z = ZipArchive::new(f)?;
+    let mut pubkey_file = z.by_name(APEX_PUBKEY_ENTRY)?;
+    let mut pubkey = Vec::new();
+    pubkey_file.read_to_end(&mut pubkey)?;
+    Ok(pubkey)
+}
+
+/// Convert vector of ApexData into Metadata
+pub fn to_metadata(apex_data: &[ApexData]) -> Metadata {
+    Metadata {
+        apexes: apex_data
+            .iter()
+            .map(|data| ApexPayload {
+                name: data.name.clone(),
+                public_key: data.pubkey.clone(),
+                ..Default::default()
+            })
+            .collect(),
+        ..Default::default()
+    }
+}
diff --git a/tests/aidl/Android.bp b/tests/aidl/Android.bp
new file mode 100644
index 0000000..893ec0b
--- /dev/null
+++ b/tests/aidl/Android.bp
@@ -0,0 +1,18 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+aidl_interface {
+    name: "com.android.microdroid.testservice",
+    srcs: ["com/android/microdroid/testservice/**/*.aidl"],
+    unstable: true,
+    backend: {
+        java: {
+            platform_apis: true,
+            gen_rpc: true,
+        },
+        cpp: {
+            enabled: true,
+        },
+    },
+}
diff --git a/compos/aidl/com/android/compos/Metadata.aidl b/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
similarity index 66%
rename from compos/aidl/com/android/compos/Metadata.aidl
rename to tests/aidl/com/android/microdroid/testservice/ITestService.aidl
index a15214d..cdcb2bd 100644
--- a/compos/aidl/com/android/compos/Metadata.aidl
+++ b/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021 The Android Open Source Project
+ * 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.
@@ -13,14 +13,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-package com.android.compos;
-
-import com.android.compos.InputFdAnnotation;
-import com.android.compos.OutputFdAnnotation;
+package com.android.microdroid.testservice;
 
 /** {@hide} */
-parcelable Metadata {
-    InputFdAnnotation[] input_fd_annotations;
-    OutputFdAnnotation[] output_fd_annotations;
+interface ITestService {
+    const int SERVICE_PORT = 5678;
+
+    /* add two integers. */
+    int addInteger(int a, int b);
 }
diff --git a/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java b/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java
index dc65899..97cd426 100644
--- a/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java
+++ b/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java
@@ -179,10 +179,10 @@
         assertTrue(apkPath.startsWith("package:"));
         apkPath = apkPath.substring("package:".length());
 
-        // Push the idsig file to the device
-        File idsigOnHost = findTestFile(buildInfo, apkName + ".idsig");
-        final String apkIdsigPath = TEST_ROOT + apkName + ".idsig";
-        androidDevice.pushFile(idsigOnHost, apkIdsigPath);
+        android.run("mkdir", "-p", TEST_ROOT);
+
+        // This file is not what we provide. It will be created by the vm tool.
+        final String outApkIdsigPath = TEST_ROOT + apkName + ".idsig";
 
         final String instanceImg = TEST_ROOT + INSTANCE_IMG;
         final String logPath = TEST_ROOT + "log.txt";
@@ -197,7 +197,7 @@
                         "--log " + logPath,
                         debugFlag,
                         apkPath,
-                        apkIdsigPath,
+                        outApkIdsigPath,
                         instanceImg,
                         configPath);
 
diff --git a/tests/testapk/Android.bp b/tests/testapk/Android.bp
index f545f8e..0b0810f 100644
--- a/tests/testapk/Android.bp
+++ b/tests/testapk/Android.bp
@@ -19,8 +19,11 @@
     srcs: ["src/native/testbinary.cpp"],
     shared_libs: [
         "android.system.keystore2-V1-ndk",
+        "android.system.virtualmachineservice-ndk",
+        "com.android.microdroid.testservice-ndk",
         "libbase",
         "libbinder_ndk",
+        "libbinder_rpc_unstable",
         "MicrodroidTestNativeLibSub",
     ],
 }
diff --git a/tests/testapk/src/native/testbinary.cpp b/tests/testapk/src/native/testbinary.cpp
index 1572021..2903a08 100644
--- a/tests/testapk/src/native/testbinary.cpp
+++ b/tests/testapk/src/native/testbinary.cpp
@@ -14,11 +14,21 @@
  * limitations under the License.
  */
 #include <aidl/android/system/keystore2/IKeystoreService.h>
+#include <aidl/android/system/virtualmachineservice/IVirtualMachineService.h>
+#include <aidl/com/android/microdroid/testservice/BnTestService.h>
 #include <android-base/result.h>
+#include <android-base/unique_fd.h>
 #include <android/binder_auto_utils.h>
 #include <android/binder_manager.h>
+#include <fcntl.h>
+#include <linux/vm_sockets.h>
+#include <stdint.h>
 #include <stdio.h>
+#include <sys/ioctl.h>
 #include <sys/system_properties.h>
+#include <unistd.h>
+
+#include <binder_rpc_unstable.hpp>
 
 using aidl::android::hardware::security::keymint::Algorithm;
 using aidl::android::hardware::security::keymint::Digest;
@@ -35,8 +45,12 @@
 using aidl::android::system::keystore2::KeyDescriptor;
 using aidl::android::system::keystore2::KeyMetadata;
 
+using aidl::android::system::virtualmachineservice::IVirtualMachineService;
+
+using android::base::ErrnoError;
 using android::base::Error;
 using android::base::Result;
+using android::base::unique_fd;
 
 extern void testlib_sub();
 
@@ -184,9 +198,63 @@
     return result;
 }
 
+Result<unsigned> get_local_cid() {
+    // TODO: remove this after VS can check the peer addresses of binder clients
+    unique_fd fd(open("/dev/vsock", O_RDONLY));
+    if (fd.get() == -1) {
+        return ErrnoError() << "failed to open /dev/vsock";
+    }
+
+    unsigned cid;
+    if (ioctl(fd.get(), IOCTL_VM_SOCKETS_GET_LOCAL_CID, &cid) == -1) {
+        return ErrnoError() << "failed to IOCTL_VM_SOCKETS_GET_LOCAL_CID";
+    }
+
+    return cid;
+}
+
+Result<void> start_test_service() {
+    class TestService : public aidl::com::android::microdroid::testservice::BnTestService {
+        ndk::ScopedAStatus addInteger(int32_t a, int32_t b, int32_t* out) override {
+            *out = a + b;
+            return ndk::ScopedAStatus::ok();
+        }
+    };
+    auto testService = ndk::SharedRefBase::make<TestService>();
+
+    auto callback = []([[maybe_unused]] void* param) {
+        // Tell microdroid_manager that we're ready.
+        // Failing to notify is not a fatal error; the payload can continue.
+        ndk::SpAIBinder binder(
+                RpcClient(VMADDR_CID_HOST, IVirtualMachineService::VM_BINDER_SERVICE_PORT));
+        auto virtualMachineService = IVirtualMachineService::fromBinder(binder);
+        if (virtualMachineService == nullptr) {
+            std::cerr << "failed to connect VirtualMachineService";
+            return;
+        }
+        if (auto res = get_local_cid(); !res.ok()) {
+            std::cerr << "failed to get local cid: " << res.error();
+        } else if (!virtualMachineService->notifyPayloadReady(res.value()).isOk()) {
+            std::cerr << "failed to notify payload ready to virtualizationservice";
+        }
+    };
+
+    if (!RunRpcServerCallback(testService->asBinder().get(), testService->SERVICE_PORT, callback,
+                              nullptr)) {
+        return Error() << "RPC Server failed to run";
+    }
+
+    return {};
+}
+
 } // Anonymous namespace
 
 extern "C" int android_native_main(int argc, char* argv[]) {
+    // disable buffering to communicate seamlessly
+    setvbuf(stdin, nullptr, _IONBF, 0);
+    setvbuf(stdout, nullptr, _IONBF, 0);
+    setvbuf(stderr, nullptr, _IONBF, 0);
+
     printf("Hello Microdroid ");
     for (int i = 0; i < argc; i++) {
         printf("%s", argv[i]);
@@ -201,5 +269,10 @@
     __system_property_set("debug.microdroid.app.run", "true");
     if (!report_test("keystore", test_keystore()).ok()) return 1;
 
-    return 0;
+    if (auto res = start_test_service(); res.ok()) {
+        return 0;
+    } else {
+        std::cerr << "starting service failed: " << res.error();
+        return 1;
+    }
 }
diff --git a/tests/vsock_test.cc b/tests/vsock_test.cc
index a594e6d..480d05a 100644
--- a/tests/vsock_test.cc
+++ b/tests/vsock_test.cc
@@ -85,14 +85,17 @@
 
     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;
+    status = virtualization_service->createVm(config, std::nullopt, &vm);
+    ASSERT_TRUE(status.isOk()) << "Error creating VM: " << status;
 
     int32_t cid;
     status = vm->getCid(&cid);
     ASSERT_TRUE(status.isOk()) << "Error getting CID: " << status;
     LOG(INFO) << "VM starting with CID " << cid;
 
+    status = vm->start();
+    ASSERT_TRUE(status.isOk()) << "Error starting VM: " << status;
+
     LOG(INFO) << "Accepting connection...";
     struct sockaddr_vm client_sa;
     socklen_t client_sa_len = sizeof(client_sa);
diff --git a/virtualizationservice/aidl/Android.bp b/virtualizationservice/aidl/Android.bp
index 974bdc6..571cc5d 100644
--- a/virtualizationservice/aidl/Android.bp
+++ b/virtualizationservice/aidl/Android.bp
@@ -18,12 +18,16 @@
         },
         ndk: {
             apex_available: [
+                "com.android.virt",
                 "com.android.compos",
             ],
         },
         rust: {
             enabled: true,
-            apex_available: ["com.android.virt"],
+            apex_available: [
+                "com.android.virt",
+                "com.android.compos",
+            ],
         },
     },
 }
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl
index 081580c..6562159 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl
@@ -16,13 +16,14 @@
 package android.system.virtualizationservice;
 
 import android.system.virtualizationservice.IVirtualMachineCallback;
+import android.system.virtualizationservice.VirtualMachineState;
 
 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();
+    /** Returns the current lifecycle state of the VM. */
+    VirtualMachineState getState();
 
     /**
      * Register a Binder object to get callbacks when the state of the VM changes, such as if it
@@ -33,6 +34,9 @@
      */
     void registerCallback(IVirtualMachineCallback callback);
 
+    /** Starts running the VM. */
+    void start();
+
     /** Open a vsock connection to the CID of the VM on the given port. */
     ParcelFileDescriptor connectVsock(int port);
 }
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
index 7c4b897..8be7331 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
@@ -22,10 +22,10 @@
 
 interface IVirtualizationService {
     /**
-     * Start the VM with the given config file, and return a handle to it. If `logFd` is provided
-     * then console logs from the VM will be sent to it.
+     * Create the VM with the given config file, and return a handle to it ready to start it. If
+     * `logFd` is provided then console logs from the VM will be sent to it.
      */
-    IVirtualMachine startVm(
+    IVirtualMachine createVm(
             in VirtualMachineConfig config, in @nullable ParcelFileDescriptor logFd);
 
     /**
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineDebugInfo.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineDebugInfo.aidl
index d081b8d..672c41a 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineDebugInfo.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineDebugInfo.aidl
@@ -15,6 +15,8 @@
  */
 package android.system.virtualizationservice;
 
+import android.system.virtualizationservice.VirtualMachineState;
+
 /** Information about a running VM, for debug purposes only. */
 parcelable VirtualMachineDebugInfo {
     /** The CID assigned to the VM. */
@@ -35,6 +37,6 @@
      */
     int requesterPid;
 
-    /** Whether the VM is still running. */
-    boolean running;
+    /** The current lifecycle state of the VM. */
+    VirtualMachineState state;
 }
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineState.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineState.aidl
new file mode 100644
index 0000000..b1aebfd
--- /dev/null
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineState.aidl
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+/**
+ * The lifecycle state of a VM.
+ */
+@Backing(type="int")
+enum VirtualMachineState {
+    /**
+     * The VM has been created but not yet started.
+     */
+    NOT_STARTED = 0,
+    /**
+     * The VM is running, but the payload has not yet started.
+     */
+    STARTING = 1,
+    /**
+     * The VM is running and the payload has been started, but it has not yet indicated that it is
+     * ready.
+     */
+    STARTED = 2,
+    /**
+     * The VM payload has indicated that it is ready to serve requests.
+     */
+    READY = 3,
+    /**
+     * The VM payload has finished but the VM itself is still running.
+     */
+    FINISHED = 4,
+    /**
+     * The VM has died.
+     */
+    DEAD = 5,
+}
diff --git a/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl b/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl
index 10b14e0..fba83c8 100644
--- a/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl
+++ b/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl
@@ -18,6 +18,18 @@
 /** {@hide} */
 interface IVirtualMachineService {
     /**
+     * Port number that VirtualMachineService listens on connections from the guest VMs for the
+     * payload input and output.
+     */
+    const int VM_STREAM_SERVICE_PORT = 3000;
+
+    /**
+     * Port number that VirtualMachineService listens on connections from the guest VMs for the
+     * VirtualMachineService binder service.
+     */
+    const int VM_BINDER_SERVICE_PORT = 5000;
+
+    /**
      * Notifies that the payload has started.
      * TODO(b/191845268): remove cid parameter
      */
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index 6b60da3..b0ea0ba 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -15,29 +15,30 @@
 //! Implementation of the AIDL interface of the VirtualizationService.
 
 use crate::composite::make_composite_image;
-use crate::crosvm::{CrosvmConfig, DiskFile, VmInstance};
+use crate::crosvm::{CrosvmConfig, DiskFile, PayloadState, VmInstance, VmState};
 use crate::payload::add_microdroid_images;
 use crate::{Cid, FIRST_GUEST_CID};
 
 use android_os_permissions_aidl::aidl::android::os::IPermissionController;
-use android_system_virtualizationservice::aidl::android::system::virtualizationservice::DiskImage::DiskImage;
-use android_system_virtualizationservice::aidl::android::system::virtualizationservice::IVirtualizationService::IVirtualizationService;
 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::{
+    DiskImage::DiskImage,
+    IVirtualMachineCallback::IVirtualMachineCallback,
+    IVirtualizationService::IVirtualizationService,
+    PartitionType::PartitionType,
     VirtualMachineAppConfig::VirtualMachineAppConfig,
     VirtualMachineConfig::VirtualMachineConfig,
+    VirtualMachineDebugInfo::VirtualMachineDebugInfo,
     VirtualMachineRawConfig::VirtualMachineRawConfig,
+    VirtualMachineState::VirtualMachineState,
 };
-use android_system_virtualizationservice::aidl::android::system::virtualizationservice::VirtualMachineDebugInfo::VirtualMachineDebugInfo;
-use android_system_virtualizationservice::aidl::android::system::virtualizationservice::PartitionType::PartitionType;
 use android_system_virtualizationservice::binder::{
     self, BinderFeatures, ExceptionCode, Interface, ParcelFileDescriptor, Status, Strong, ThreadState,
 };
 use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice::IVirtualMachineService::{
-    BnVirtualMachineService, IVirtualMachineService,
+    VM_BINDER_SERVICE_PORT, VM_STREAM_SERVICE_PORT, BnVirtualMachineService, IVirtualMachineService,
 };
 use anyhow::{bail, Context, Result};
 use ::binder::unstable_api::AsNative;
@@ -50,7 +51,7 @@
 use std::fs::{File, OpenOptions, create_dir};
 use std::io::{Error, ErrorKind, Write};
 use std::num::NonZeroU32;
-use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd};
+use std::os::unix::io::{FromRawFd, IntoRawFd};
 use std::path::{Path, PathBuf};
 use std::sync::{Arc, Mutex, Weak};
 use vmconfig::VmConfig;
@@ -65,15 +66,6 @@
 /// The CID representing the host VM
 const VMADDR_CID_HOST: u32 = 2;
 
-/// Port number that virtualizationservice listens on connections from the guest VMs for the
-/// payload input and output
-const PORT_VIRT_STREAM_SERVICE: u32 = 3000;
-
-/// Port number that virtualizationservice listens on connections from the guest VMs for the
-/// VirtualMachineService binder service
-/// Sync with microdroid_manager/src/main.rs
-const PORT_VM_BINDER_SERVICE: u32 = 5000;
-
 /// The size of zero.img.
 /// Gaps in composite disk images are filled with a shared zero.img.
 const ZERO_FILLER_SIZE: u64 = 4096;
@@ -93,10 +85,11 @@
 impl Interface for VirtualizationService {}
 
 impl IVirtualizationService for VirtualizationService {
-    /// Create and start a new VM with the given configuration, assigning it the next available CID.
+    /// Creates (but does not start) a new VM with the given configuration, assigning it the next
+    /// available CID.
     ///
     /// Returns a binder `IVirtualMachine` object referring to it, as a handle for the client.
-    fn startVm(
+    fn createVm(
         &self,
         config: &VirtualMachineConfig,
         log_fd: Option<&ParcelFileDescriptor>,
@@ -172,32 +165,32 @@
         // Actually start the VM.
         let crosvm_config = CrosvmConfig {
             cid,
-            bootloader: as_asref(&config.bootloader),
-            kernel: as_asref(&config.kernel),
-            initrd: as_asref(&config.initrd),
+            bootloader: maybe_clone_file(&config.bootloader)?,
+            kernel: maybe_clone_file(&config.kernel)?,
+            initrd: maybe_clone_file(&config.initrd)?,
             disks,
             params: config.params.to_owned(),
             protected: config.protectedVm,
             memory_mib: config.memoryMib.try_into().ok().and_then(NonZeroU32::new),
-        };
-        let composite_disk_fds: Vec<_> =
-            indirect_files.iter().map(|file| file.as_raw_fd()).collect();
-        let instance = VmInstance::start(
-            &crosvm_config,
             log_fd,
-            &composite_disk_fds,
-            temporary_directory,
-            requester_uid,
-            requester_sid,
-            requester_debug_pid,
-        )
-        .map_err(|e| {
-            error!("Failed to start VM with config {:?}: {}", config, e);
-            new_binder_exception(
-                ExceptionCode::SERVICE_SPECIFIC,
-                format!("Failed to start VM: {}", e),
+            indirect_files,
+        };
+        let instance = Arc::new(
+            VmInstance::new(
+                crosvm_config,
+                temporary_directory,
+                requester_uid,
+                requester_sid,
+                requester_debug_pid,
             )
-        })?;
+            .map_err(|e| {
+                error!("Failed to create VM with config {:?}: {}", config, e);
+                new_binder_exception(
+                    ExceptionCode::SERVICE_SPECIFIC,
+                    format!("Failed to create VM: {}", e),
+                )
+            })?,
+        );
         state.add_vm(Arc::downgrade(&instance));
         Ok(VirtualMachine::create(instance))
     }
@@ -276,7 +269,7 @@
                 requesterUid: vm.requester_uid as i32,
                 requesterSid: vm.requester_sid.clone(),
                 requesterPid: vm.requester_debug_pid,
-                running: vm.running(),
+                state: get_state(&vm),
             })
             .collect();
         Ok(cids)
@@ -323,7 +316,7 @@
             let retval = unsafe {
                 binder_rpc_unstable_bindgen::RunRpcServer(
                     service.as_native_mut() as *mut binder_rpc_unstable_bindgen::AIBinder,
-                    PORT_VM_BINDER_SERVICE,
+                    VM_BINDER_SERVICE_PORT as u32,
                 )
             };
             if retval {
@@ -338,10 +331,11 @@
     }
 }
 
-/// Waits for incoming connections from VM. If a new connection is made, notify the event to the
-/// client via the callback (if registered).
+/// Waits for incoming connections from VM. If a new connection is made, stores the stream in the
+/// corresponding `VmInstance`.
 fn handle_stream_connection_from_vm(state: Arc<Mutex<State>>) -> Result<()> {
-    let listener = VsockListener::bind_with_cid_port(VMADDR_CID_HOST, PORT_VIRT_STREAM_SERVICE)?;
+    let listener =
+        VsockListener::bind_with_cid_port(VMADDR_CID_HOST, VM_STREAM_SERVICE_PORT as u32)?;
     for stream in listener.incoming() {
         let stream = match stream {
             Err(e) => {
@@ -355,7 +349,7 @@
             let port = addr.port();
             info!("payload stream connected from cid={}, port={}", cid, port);
             if let Some(vm) = state.lock().unwrap().get_vm(cid) {
-                vm.stream.lock().unwrap().insert(stream);
+                *vm.stream.lock().unwrap() = Some(stream);
             } else {
                 error!("connection from cid={} is not from a guest VM", cid);
             }
@@ -522,8 +516,8 @@
                 }
             }
         } else {
-            error!("Missing SID on startVm");
-            Err(new_binder_exception(ExceptionCode::SECURITY, "Missing SID on startVm"))
+            error!("Missing SID on createVm");
+            Err(new_binder_exception(ExceptionCode::SECURITY, "Missing SID on createVm"))
         }
     })
 }
@@ -580,10 +574,10 @@
         Ok(self.instance.cid as i32)
     }
 
-    fn isRunning(&self) -> binder::Result<bool> {
+    fn getState(&self) -> binder::Result<VirtualMachineState> {
         // Don't check permission. The owner of the VM might have passed this binder object to
         // others.
-        Ok(self.instance.running())
+        Ok(get_state(&self.instance))
     }
 
     fn registerCallback(
@@ -598,12 +592,16 @@
         Ok(())
     }
 
+    fn start(&self) -> binder::Result<()> {
+        self.instance.start().map_err(|e| {
+            error!("Error starting VM with CID {}: {:?}", self.instance.cid, e);
+            new_binder_exception(ExceptionCode::SERVICE_SPECIFIC, e.to_string())
+        })
+    }
+
     fn connectVsock(&self, port: i32) -> binder::Result<ParcelFileDescriptor> {
-        if !self.instance.running() {
-            return Err(new_binder_exception(
-                ExceptionCode::SERVICE_SPECIFIC,
-                "VM is no longer running",
-            ));
+        if !matches!(&*self.instance.vm_state.lock().unwrap(), VmState::Running { .. }) {
+            return Err(new_binder_exception(ExceptionCode::SERVICE_SPECIFIC, "VM is not running"));
         }
         let stream =
             VsockStream::connect_with_cid_port(self.instance.cid, port as u32).map_err(|e| {
@@ -741,9 +739,19 @@
     }
 }
 
-/// Converts an `&Option<T>` to an `Option<U>` where `T` implements `AsRef<U>`.
-fn as_asref<T: AsRef<U>, U>(option: &Option<T>) -> Option<&U> {
-    option.as_ref().map(|t| t.as_ref())
+/// Gets the `VirtualMachineState` of the given `VmInstance`.
+fn get_state(instance: &VmInstance) -> VirtualMachineState {
+    match &*instance.vm_state.lock().unwrap() {
+        VmState::NotStarted { .. } => VirtualMachineState::NOT_STARTED,
+        VmState::Running { .. } => match instance.payload_state() {
+            PayloadState::Starting => VirtualMachineState::STARTING,
+            PayloadState::Started => VirtualMachineState::STARTED,
+            PayloadState::Ready => VirtualMachineState::READY,
+            PayloadState::Finished => VirtualMachineState::FINISHED,
+        },
+        VmState::Dead => VirtualMachineState::DEAD,
+        VmState::Failed => VirtualMachineState::DEAD,
+    }
 }
 
 /// Converts a `&ParcelFileDescriptor` to a `File` by cloning the file.
@@ -756,6 +764,11 @@
     })
 }
 
+/// Converts an `&Option<ParcelFileDescriptor>` to an `Option<File>` by cloning the file.
+fn maybe_clone_file(file: &Option<ParcelFileDescriptor>) -> Result<Option<File>, Status> {
+    file.as_ref().map(clone_file).transpose()
+}
+
 /// Converts a `VsockStream` to a `ParcelFileDescriptor`.
 fn vsock_stream_to_pfd(stream: VsockStream) -> ParcelFileDescriptor {
     // SAFETY: ownership is transferred from stream to f
@@ -797,6 +810,8 @@
         let cid = cid as Cid;
         if let Some(vm) = self.state.lock().unwrap().get_vm(cid) {
             info!("VM having CID {} started payload", cid);
+            vm.update_payload_state(PayloadState::Started)
+                .map_err(|e| new_binder_exception(ExceptionCode::ILLEGAL_STATE, e.to_string()))?;
             let stream = vm.stream.lock().unwrap().take();
             vm.callbacks.notify_payload_started(cid, stream);
             Ok(())
@@ -813,6 +828,8 @@
         let cid = cid as Cid;
         if let Some(vm) = self.state.lock().unwrap().get_vm(cid) {
             info!("VM having CID {} payload is ready", cid);
+            vm.update_payload_state(PayloadState::Ready)
+                .map_err(|e| new_binder_exception(ExceptionCode::ILLEGAL_STATE, e.to_string()))?;
             vm.callbacks.notify_payload_ready(cid);
             Ok(())
         } else {
@@ -828,6 +845,8 @@
         let cid = cid as Cid;
         if let Some(vm) = self.state.lock().unwrap().get_vm(cid) {
             info!("VM having CID {} finished payload", cid);
+            vm.update_payload_state(PayloadState::Finished)
+                .map_err(|e| new_binder_exception(ExceptionCode::ILLEGAL_STATE, e.to_string()))?;
             vm.callbacks.notify_payload_finished(cid, exit_code);
             Ok(())
         } else {
diff --git a/virtualizationservice/src/crosvm.rs b/virtualizationservice/src/crosvm.rs
index 5984ff0..38e5bf3 100644
--- a/virtualizationservice/src/crosvm.rs
+++ b/virtualizationservice/src/crosvm.rs
@@ -21,11 +21,11 @@
 use log::{debug, error, info};
 use shared_child::SharedChild;
 use std::fs::{remove_dir_all, File};
+use std::mem;
 use std::num::NonZeroU32;
 use std::os::unix::io::{AsRawFd, RawFd};
 use std::path::PathBuf;
 use std::process::Command;
-use std::sync::atomic::{AtomicBool, Ordering};
 use std::sync::{Arc, Mutex};
 use std::thread;
 use vsock::VsockStream;
@@ -34,15 +34,17 @@
 
 /// Configuration for a VM to run with crosvm.
 #[derive(Debug)]
-pub struct CrosvmConfig<'a> {
+pub struct CrosvmConfig {
     pub cid: Cid,
-    pub bootloader: Option<&'a File>,
-    pub kernel: Option<&'a File>,
-    pub initrd: Option<&'a File>,
+    pub bootloader: Option<File>,
+    pub kernel: Option<File>,
+    pub initrd: Option<File>,
     pub disks: Vec<DiskFile>,
     pub params: Option<String>,
     pub protected: bool,
     pub memory_mib: Option<NonZeroU32>,
+    pub log_fd: Option<File>,
+    pub indirect_files: Vec<File>,
 }
 
 /// A disk image to pass to crosvm for a VM.
@@ -52,11 +54,67 @@
     pub writable: bool,
 }
 
-/// Information about a particular instance of a VM which is running.
+/// The lifecycle state which the payload in the VM has reported itself to be in.
+///
+/// Note that the order of enum variants is significant; only forward transitions are allowed by
+/// [`VmInstance::update_payload_state`].
+#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
+pub enum PayloadState {
+    Starting,
+    Started,
+    Ready,
+    Finished,
+}
+
+/// The current state of the VM itself.
+#[derive(Debug)]
+pub enum VmState {
+    /// The VM has not yet tried to start.
+    NotStarted {
+        ///The configuration needed to start the VM, if it has not yet been started.
+        config: CrosvmConfig,
+    },
+    /// The VM has been started.
+    Running {
+        /// The crosvm child process.
+        child: Arc<SharedChild>,
+    },
+    /// The VM died or was killed.
+    Dead,
+    /// The VM failed to start.
+    Failed,
+}
+
+impl VmState {
+    /// Tries to start the VM, if it is in the `NotStarted` state.
+    ///
+    /// Returns an error if the VM is in the wrong state, or fails to start.
+    fn start(&mut self, instance: Arc<VmInstance>) -> Result<(), Error> {
+        let state = mem::replace(self, VmState::Failed);
+        if let VmState::NotStarted { config } = state {
+            // If this fails and returns an error, `self` will be left in the `Failed` state.
+            let child = Arc::new(run_vm(config)?);
+
+            let child_clone = child.clone();
+            thread::spawn(move || {
+                instance.monitor(child_clone);
+            });
+
+            // If it started correctly, update the state.
+            *self = VmState::Running { child };
+            Ok(())
+        } else {
+            *self = state;
+            bail!("VM already started or failed")
+        }
+    }
+}
+
+/// Information about a particular instance of a VM which may be running.
 #[derive(Debug)]
 pub struct VmInstance {
-    /// The crosvm child process.
-    child: SharedChild,
+    /// The current state of the VM.
+    pub vm_state: Mutex<VmState>,
     /// The CID assigned to the VM for vsock communication.
     pub cid: Cid,
     /// Whether the VM is a protected VM.
@@ -70,77 +128,62 @@
     /// 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_debug_pid: i32,
-    /// Whether the VM is still running.
-    running: AtomicBool,
     /// Callbacks to clients of the VM.
     pub callbacks: VirtualMachineCallbacks,
     /// Input/output stream of the payload run in the VM.
     pub stream: Mutex<Option<VsockStream>>,
+    /// The latest lifecycle state which the payload reported itself to be in.
+    payload_state: Mutex<PayloadState>,
 }
 
 impl VmInstance {
-    /// Create a new `VmInstance` for the given process.
-    fn new(
-        child: SharedChild,
-        cid: Cid,
-        protected: bool,
+    /// Validates the given config and creates a new `VmInstance` but doesn't start running it.
+    pub fn new(
+        config: CrosvmConfig,
         temporary_directory: PathBuf,
         requester_uid: u32,
         requester_sid: String,
         requester_debug_pid: i32,
-    ) -> VmInstance {
-        VmInstance {
-            child,
+    ) -> Result<VmInstance, Error> {
+        validate_config(&config)?;
+        let cid = config.cid;
+        let protected = config.protected;
+        Ok(VmInstance {
+            vm_state: Mutex::new(VmState::NotStarted { config }),
             cid,
             protected,
             temporary_directory,
             requester_uid,
             requester_sid,
             requester_debug_pid,
-            running: AtomicBool::new(true),
             callbacks: Default::default(),
             stream: Mutex::new(None),
-        }
+            payload_state: Mutex::new(PayloadState::Starting),
+        })
     }
 
-    /// Start an instance of `crosvm` to manage a new VM. The `crosvm` instance will be killed when
+    /// Starts an instance of `crosvm` to manage the VM. The `crosvm` instance will be killed when
     /// the `VmInstance` is dropped.
-    pub fn start(
-        config: &CrosvmConfig,
-        log_fd: Option<File>,
-        composite_disk_fds: &[RawFd],
-        temporary_directory: PathBuf,
-        requester_uid: u32,
-        requester_sid: String,
-        requester_debug_pid: i32,
-    ) -> Result<Arc<VmInstance>, Error> {
-        let child = run_vm(config, log_fd, composite_disk_fds)?;
-        let instance = Arc::new(VmInstance::new(
-            child,
-            config.cid,
-            config.protected,
-            temporary_directory,
-            requester_uid,
-            requester_sid,
-            requester_debug_pid,
-        ));
-
-        let instance_clone = instance.clone();
-        thread::spawn(move || {
-            instance_clone.monitor();
-        });
-
-        Ok(instance)
+    pub fn start(self: &Arc<Self>) -> Result<(), Error> {
+        self.vm_state.lock().unwrap().start(self.clone())
     }
 
-    /// 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() {
+    /// Waits for the crosvm child process to finish, then marks the VM as no longer running and
+    /// calls any callbacks.
+    ///
+    /// This takes a separate reference to the `SharedChild` rather than using the one in
+    /// `self.vm_state` to avoid holding the lock on `vm_state` while it is running.
+    fn monitor(&self, child: Arc<SharedChild>) {
+        match 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);
+
+        let mut vm_state = self.vm_state.lock().unwrap();
+        *vm_state = VmState::Dead;
+        // Ensure that the mutex is released before calling the callbacks.
+        drop(vm_state);
+
         self.callbacks.callback_on_died(self.cid);
 
         // Delete temporary files.
@@ -149,27 +192,39 @@
         }
     }
 
-    /// Return whether `crosvm` is still running the VM.
-    pub fn running(&self) -> bool {
-        self.running.load(Ordering::Acquire)
+    /// Returns the last reported state of the VM payload.
+    pub fn payload_state(&self) -> PayloadState {
+        *self.payload_state.lock().unwrap()
     }
 
-    /// Kill the crosvm instance.
+    /// Updates the payload state to the given value, if it is a valid state transition.
+    pub fn update_payload_state(&self, new_state: PayloadState) -> Result<(), Error> {
+        let mut state_locked = self.payload_state.lock().unwrap();
+        // Only allow forward transitions, e.g. from starting to started or finished, not back in
+        // the other direction.
+        if new_state > *state_locked {
+            *state_locked = new_state;
+            Ok(())
+        } else {
+            bail!("Invalid payload state transition from {:?} to {:?}", *state_locked, new_state)
+        }
+    }
+
+    /// Kills the crosvm instance, if it is running.
     pub fn kill(&self) {
-        // TODO: Talk to crosvm to shutdown cleanly.
-        if let Err(e) = self.child.kill() {
-            error!("Error killing crosvm instance: {}", e);
+        let vm_state = &*self.vm_state.lock().unwrap();
+        if let VmState::Running { child } = vm_state {
+            // TODO: Talk to crosvm to shutdown cleanly.
+            if let Err(e) = child.kill() {
+                error!("Error killing crosvm instance: {}", e);
+            }
         }
     }
 }
 
-/// Start an instance of `crosvm` to manage a new VM.
-fn run_vm(
-    config: &CrosvmConfig,
-    log_fd: Option<File>,
-    composite_disk_fds: &[RawFd],
-) -> Result<SharedChild, Error> {
-    validate_config(config)?;
+/// Starts an instance of `crosvm` to manage a new VM.
+fn run_vm(config: CrosvmConfig) -> Result<SharedChild, Error> {
+    validate_config(&config)?;
 
     let mut command = Command::new(CROSVM_PATH);
     // TODO(qwandor): Remove --disable-sandbox.
@@ -183,7 +238,7 @@
         command.arg("--mem").arg(memory_mib.to_string());
     }
 
-    if let Some(log_fd) = log_fd {
+    if let Some(log_fd) = config.log_fd {
         command.stdout(log_fd);
     } else {
         // Ignore console output.
@@ -191,7 +246,7 @@
     }
 
     // Keep track of what file descriptors should be mapped to the crosvm process.
-    let mut preserved_fds = composite_disk_fds.to_vec();
+    let mut preserved_fds = config.indirect_files.iter().map(|file| file.as_raw_fd()).collect();
 
     if let Some(bootloader) = &config.bootloader {
         command.arg("--bios").arg(add_preserved_fd(&mut preserved_fds, bootloader));
diff --git a/virtualizationservice/src/main.rs b/virtualizationservice/src/main.rs
index 018be7b..8628c01 100644
--- a/virtualizationservice/src/main.rs
+++ b/virtualizationservice/src/main.rs
@@ -65,3 +65,12 @@
     }
     Ok(())
 }
+
+#[cfg(test)]
+mod tests {
+    /// We need to have at least one test to avoid errors running the test suite, so this is a
+    /// placeholder until we have real tests.
+    #[test]
+    #[ignore]
+    fn placeholder() {}
+}
diff --git a/virtualizationservice/src/payload.rs b/virtualizationservice/src/payload.rs
index 75ba6c7..9662fa3 100644
--- a/virtualizationservice/src/payload.rs
+++ b/virtualizationservice/src/payload.rs
@@ -31,8 +31,7 @@
 
 /// The list of APEXes which microdroid requires.
 // TODO(b/192200378) move this to microdroid.json?
-const MICRODROID_REQUIRED_APEXES: [&str; 3] =
-    ["com.android.adbd", "com.android.i18n", "com.android.os.statsd"];
+const MICRODROID_REQUIRED_APEXES: [&str; 2] = ["com.android.adbd", "com.android.os.statsd"];
 
 const APEX_INFO_LIST_PATH: &str = "/apex/apex-info-list.xml";
 
diff --git a/vm/src/run.rs b/vm/src/run.rs
index ccb4085..0d34a97 100644
--- a/vm/src/run.rs
+++ b/vm/src/run.rs
@@ -16,15 +16,16 @@
 
 use crate::create_partition::command_create_partition;
 use crate::sync::AtomicFlag;
-use android_system_virtualizationservice::aidl::android::system::virtualizationservice::IVirtualizationService::IVirtualizationService;
-use android_system_virtualizationservice::aidl::android::system::virtualizationservice::IVirtualMachine::IVirtualMachine;
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::IVirtualMachineCallback::{
     BnVirtualMachineCallback, IVirtualMachineCallback,
 };
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
+    IVirtualMachine::IVirtualMachine,
+    IVirtualizationService::IVirtualizationService,
     PartitionType::PartitionType,
     VirtualMachineAppConfig::VirtualMachineAppConfig,
     VirtualMachineConfig::VirtualMachineConfig,
+    VirtualMachineState::VirtualMachineState,
 };
 use android_system_virtualizationservice::binder::{
     BinderFeatures, DeathRecipient, IBinder, ParcelFileDescriptor, Strong,
@@ -100,6 +101,18 @@
     )
 }
 
+fn state_to_str(vm_state: VirtualMachineState) -> &'static str {
+    match vm_state {
+        VirtualMachineState::NOT_STARTED => "NOT_STARTED",
+        VirtualMachineState::STARTING => "STARTING",
+        VirtualMachineState::STARTED => "STARTED",
+        VirtualMachineState::READY => "READY",
+        VirtualMachineState::FINISHED => "FINISHED",
+        VirtualMachineState::DEAD => "DEAD",
+        _ => "(invalid state)",
+    }
+}
+
 fn run(
     service: Strong<dyn IVirtualizationService>,
     config: &VirtualMachineConfig,
@@ -117,10 +130,17 @@
     } else {
         Some(ParcelFileDescriptor::new(duplicate_stdout()?))
     };
-    let vm = service.startVm(config, stdout.as_ref()).context("Failed to start VM")?;
+    let vm = service.createVm(config, stdout.as_ref()).context("Failed to create VM")?;
 
     let cid = vm.getCid().context("Failed to get CID")?;
-    println!("Started VM from {} with CID {}.", config_path, cid);
+    println!(
+        "Created VM from {} with CID {}, state is {}.",
+        config_path,
+        cid,
+        state_to_str(vm.getState()?)
+    );
+    vm.start()?;
+    println!("Started VM, state now {}.", state_to_str(vm.getState()?));
 
     if daemonize {
         // Pass the VM reference back to VirtualizationService and have it hold it in the
diff --git a/zipfuse/Android.bp b/zipfuse/Android.bp
index 46f4b5a..79e6bad 100644
--- a/zipfuse/Android.bp
+++ b/zipfuse/Android.bp
@@ -29,6 +29,7 @@
     name: "zipfuse",
     defaults: ["zipfuse.defaults"],
     init_rc: ["zipfuse.rc"],
+    bootstrap: true,
 }
 
 rust_test {