Merge "Depend on KeyMint HAL via default"
diff --git a/virtualizationservice/.gitignore b/.gitignore
similarity index 100%
rename from virtualizationservice/.gitignore
rename to .gitignore
diff --git a/apex/Android.bp b/apex/Android.bp
index 9d6cc94..a2f272e 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -15,6 +15,7 @@
 
     // TODO(jiyong): make it updatable
     updatable: false,
+    future_updatable: true,
     platform_apis: true,
 
     system_ext_specific: true,
diff --git a/apex/product_packages.mk b/apex/product_packages.mk
index 1a431d5..322b73e 100644
--- a/apex/product_packages.mk
+++ b/apex/product_packages.mk
@@ -25,7 +25,6 @@
 
 # TODO(b/207336449): Figure out how to get these off /system
 PRODUCT_ARTIFACT_PATH_REQUIREMENT_ALLOWED_LIST := \
-    system/lib64/libgfxstream_backend.so \
     system/framework/oat/%@service-compos.jar@classes.odex \
     system/framework/oat/%@service-compos.jar@classes.vdex \
 
diff --git a/apkdmverity/Android.bp b/apkdmverity/Android.bp
index e6b1ca9..403e726 100644
--- a/apkdmverity/Android.bp
+++ b/apkdmverity/Android.bp
@@ -14,6 +14,7 @@
         "libclap",
         "libdata_model",
         "libidsig",
+        "libitertools",
         "liblibc",
         "libnix",
         "libnum_traits",
@@ -32,7 +33,6 @@
 rust_binary {
     name: "apkdmverity",
     defaults: ["apkdmverity.defaults"],
-    init_rc: ["apkdmverity.rc"],
     bootstrap: true,
 }
 
diff --git a/apkdmverity/apkdmverity.rc b/apkdmverity/apkdmverity.rc
deleted file mode 100644
index c6ef52b..0000000
--- a/apkdmverity/apkdmverity.rc
+++ /dev/null
@@ -1,3 +0,0 @@
-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 b240c85..a8a8f15 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 itertools::Itertools;
 use rustutils::system_properties;
 use std::fmt::Debug;
 use std::fs;
@@ -38,42 +39,35 @@
 fn main() -> Result<()> {
     let matches = App::new("apkdmverity")
         .about("Creates a dm-verity block device out of APK signed with APK signature scheme V4.")
-        .arg(
-            Arg::with_name("apk")
-                .help("Input APK file. Must be signed using the APK signature scheme V4.")
-                .required(true),
-        )
-        .arg(
-            Arg::with_name("idsig")
-                .help("The idsig file having the merkle tree and the signing info.")
-                .required(true),
-        )
-        .arg(
-            Arg::with_name("name")
-                .help(
-                    "Name of the dm-verity block device. The block device is created at \
-                      \"/dev/mapper/<name>\".",
-                )
-                .required(true),
-        )
+        .arg(Arg::from_usage(
+            "--apk... <apk_path> <idsig_path> <name> \
+                            'Input APK file, idsig file, and the name of the block device. The APK \
+                            file must be signed using the APK signature scheme 4. The block device \
+                            is created at \"/dev/mapper/<name>\".'",
+        ))
         .arg(Arg::with_name("verbose").short("v").long("verbose").help("Shows verbose output"))
         .get_matches();
 
-    let apk = matches.value_of("apk").unwrap();
-    let idsig = matches.value_of("idsig").unwrap();
-    let name = matches.value_of("name").unwrap();
+    let apks = matches.values_of("apk").unwrap();
+    assert!(apks.len() % 3 == 0);
+
     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: {:?}",
-            ret.data_device, ret.hash_device, ret.mapper_device
-        );
+
+    let verbose = matches.is_present("verbose");
+
+    for (apk, idsig, name) in apks.tuples() {
+        let ret = enable_verity(apk, idsig, name, roothash.as_deref())?;
+        if verbose {
+            println!(
+                "data_device: {:?}, hash_device: {:?}, mapper_device: {:?}",
+                ret.data_device, ret.hash_device, ret.mapper_device
+            );
+        }
     }
     Ok(())
 }
diff --git a/apkverify/src/sigutil.rs b/apkverify/src/sigutil.rs
index fc92898..23dd91e 100644
--- a/apkverify/src/sigutil.rs
+++ b/apkverify/src/sigutil.rs
@@ -104,8 +104,8 @@
             let mut data = data(self)?;
             while data.limit() > 0 {
                 let chunk_size = min(CHUNK_SIZE_BYTES, data.limit());
-                let mut slice = &mut chunk[..(chunk_size as usize)];
-                data.read_exact(&mut slice)?;
+                let slice = &mut chunk[..(chunk_size as usize)];
+                data.read_exact(slice)?;
                 digests_of_chunks.put_slice(
                     digester.digest(slice, CHUNK_HEADER_MID, chunk_size as u32).as_ref(),
                 );
diff --git a/authfs/aidl/com/android/virt/fs/IVirtFdService.aidl b/authfs/aidl/com/android/virt/fs/IVirtFdService.aidl
index bf4ac61..64828fb 100644
--- a/authfs/aidl/com/android/virt/fs/IVirtFdService.aidl
+++ b/authfs/aidl/com/android/virt/fs/IVirtFdService.aidl
@@ -79,4 +79,23 @@
      * @return file FD that represents the new created directory.
      */
     int createDirectoryInDirectory(int dirFd, String basename);
+
+    /** Filesystem stats that AuthFS is interested in.*/
+    parcelable FsStat {
+        /** Block size of the filesystem */
+        long blockSize;
+        /** Fragment size of the filesystem */
+        long fragmentSize;
+        /** Number of blocks in the filesystem */
+        long blockNumbers;
+        /** Number of free blocks */
+        long blockAvailable;
+        /** Number of free inodes */
+        long inodesAvailable;
+        /** Maximum filename length */
+        long maxFilename;
+    }
+
+    /** Returns relevant filesystem stats. */
+    FsStat statfs();
 }
diff --git a/authfs/fd_server/src/aidl.rs b/authfs/fd_server/src/aidl.rs
index fa1914a..ddac2bc 100644
--- a/authfs/fd_server/src/aidl.rs
+++ b/authfs/fd_server/src/aidl.rs
@@ -18,6 +18,7 @@
 use log::error;
 use nix::{
     dir::Dir, errno::Errno, fcntl::openat, fcntl::OFlag, sys::stat::mkdirat, sys::stat::Mode,
+    sys::statvfs::statvfs, sys::statvfs::Statvfs,
 };
 use std::cmp::min;
 use std::collections::{btree_map, BTreeMap};
@@ -31,7 +32,7 @@
 
 use crate::fsverity;
 use authfs_aidl_interface::aidl::com::android::virt::fs::IVirtFdService::{
-    BnVirtFdService, IVirtFdService, MAX_REQUESTING_DATA,
+    BnVirtFdService, FsStat::FsStat, IVirtFdService, MAX_REQUESTING_DATA,
 };
 use authfs_aidl_interface::binder::{
     BinderFeatures, ExceptionCode, Interface, Result as BinderResult, Status, StatusCode, Strong,
@@ -109,8 +110,8 @@
         F: FnOnce(&mut FdConfig) -> BinderResult<(i32, FdConfig)>,
     {
         let mut fd_pool = self.fd_pool.lock().unwrap();
-        let mut fd_config = fd_pool.get_mut(&fd).ok_or_else(|| new_errno_error(Errno::EBADF))?;
-        let (new_fd, new_fd_config) = create_fn(&mut fd_config)?;
+        let fd_config = fd_pool.get_mut(&fd).ok_or_else(|| new_errno_error(Errno::EBADF))?;
+        let (new_fd, new_fd_config) = create_fn(fd_config)?;
         if let btree_map::Entry::Vacant(entry) = fd_pool.entry(new_fd) {
             entry.insert(new_fd_config);
             Ok(new_fd)
@@ -331,6 +332,22 @@
             _ => Err(new_errno_error(Errno::ENOTDIR)),
         })
     }
+
+    fn statfs(&self) -> BinderResult<FsStat> {
+        let st = statvfs("/data").map_err(new_errno_error)?;
+        try_into_fs_stat(st).map_err(|_e| new_errno_error(Errno::EINVAL))
+    }
+}
+
+fn try_into_fs_stat(st: Statvfs) -> Result<FsStat, std::num::TryFromIntError> {
+    Ok(FsStat {
+        blockSize: st.block_size().try_into()?,
+        fragmentSize: st.fragment_size().try_into()?,
+        blockNumbers: st.blocks().try_into()?,
+        blockAvailable: st.blocks_available().try_into()?,
+        inodesAvailable: st.files_available().try_into()?,
+        maxFilename: st.name_max().try_into()?,
+    })
 }
 
 fn read_into_buf(file: &File, max_size: usize, offset: u64) -> io::Result<Vec<u8>> {
diff --git a/authfs/src/fsstat.rs b/authfs/src/fsstat.rs
new file mode 100644
index 0000000..81eaca1
--- /dev/null
+++ b/authfs/src/fsstat.rs
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ */
+
+use log::error;
+use std::convert::TryInto;
+use std::io;
+
+use crate::file::VirtFdService;
+use authfs_aidl_interface::aidl::com::android::virt::fs::IVirtFdService::FsStat::FsStat;
+
+/// Relevant/interesting stats of a remote filesystem.
+pub struct RemoteFsStats {
+    /// Block size of the filesystem
+    pub block_size: u64,
+    /// Fragment size of the filesystem
+    pub fragment_size: u64,
+    /// Number of blocks in the filesystem
+    pub block_numbers: u64,
+    /// Number of free blocks
+    pub block_available: u64,
+    /// Number of free inodes
+    pub inodes_available: u64,
+    /// Maximum filename length
+    pub max_filename: u64,
+}
+
+pub struct RemoteFsStatsReader {
+    service: VirtFdService,
+}
+
+impl RemoteFsStatsReader {
+    pub fn new(service: VirtFdService) -> Self {
+        Self { service }
+    }
+
+    pub fn statfs(&self) -> io::Result<RemoteFsStats> {
+        let st = self.service.statfs().map_err(|e| {
+            error!("Failed to call statfs on fd_server: {:?}", e);
+            io::Error::from_raw_os_error(libc::EIO)
+        })?;
+        try_into_remote_fs_stats(st).map_err(|_| {
+            error!("Received invalid stats from fd_server");
+            io::Error::from_raw_os_error(libc::EIO)
+        })
+    }
+}
+
+fn try_into_remote_fs_stats(st: FsStat) -> Result<RemoteFsStats, std::num::TryFromIntError> {
+    Ok(RemoteFsStats {
+        block_size: st.blockSize.try_into()?,
+        fragment_size: st.fragmentSize.try_into()?,
+        block_numbers: st.blockNumbers.try_into()?,
+        block_available: st.blockAvailable.try_into()?,
+        inodes_available: st.inodesAvailable.try_into()?,
+        max_filename: st.maxFilename.try_into()?,
+    })
+}
diff --git a/authfs/src/fusefs.rs b/authfs/src/fusefs.rs
index b456f33..8ca82f8 100644
--- a/authfs/src/fusefs.rs
+++ b/authfs/src/fusefs.rs
@@ -14,16 +14,17 @@
  * limitations under the License.
  */
 
+mod mount;
+
 use anyhow::{anyhow, bail, Result};
 use log::{debug, warn};
 use std::collections::{btree_map, BTreeMap};
 use std::convert::TryFrom;
 use std::ffi::{CStr, OsStr};
-use std::fs::OpenOptions;
 use std::io;
-use std::mem::MaybeUninit;
+use std::mem::{zeroed, MaybeUninit};
 use std::option::Option;
-use std::os::unix::{ffi::OsStrExt, io::AsRawFd};
+use std::os::unix::ffi::OsStrExt;
 use std::path::{Component, Path, PathBuf};
 use std::sync::atomic::{AtomicU64, Ordering};
 use std::sync::Mutex;
@@ -33,29 +34,24 @@
     Context, DirEntry, DirectoryIterator, Entry, FileSystem, FsOptions, GetxattrReply,
     SetattrValid, ZeroCopyReader, ZeroCopyWriter,
 };
-use fuse::mount::MountOption;
 
 use crate::common::{divide_roundup, ChunkedSizeIter, CHUNK_SIZE};
 use crate::file::{
     validate_basename, InMemoryDir, RandomWrite, ReadByChunk, RemoteDirEditor, RemoteFileEditor,
     RemoteFileReader, RemoteMerkleTreeReader,
 };
+use crate::fsstat::RemoteFsStatsReader;
 use crate::fsverity::{VerifiedFileEditor, VerifiedFileReader};
 
+pub use self::mount::mount_and_enter_message_loop;
+use self::mount::MAX_WRITE_BYTES;
+
 pub type Inode = u64;
 type Handle = u64;
 
 const DEFAULT_METADATA_TIMEOUT: Duration = Duration::from_secs(5);
 const ROOT_INODE: Inode = 1;
 
-/// Maximum bytes in the write transaction to the FUSE device. This limits the maximum buffer
-/// size in a read request (including FUSE protocol overhead) that the filesystem writes to.
-const MAX_WRITE_BYTES: u32 = 65536;
-
-/// Maximum bytes in a read operation.
-/// TODO(victorhsieh): This option is deprecated by FUSE. Figure out if we can remove this.
-const MAX_READ_BYTES: u32 = 65536;
-
 /// `AuthFsEntry` defines the filesystem entry type supported by AuthFS.
 pub enum AuthFsEntry {
     /// A read-only directory (writable during initialization). Root directory is an example.
@@ -66,7 +62,7 @@
         reader: VerifiedFileReader<RemoteFileReader, RemoteMerkleTreeReader>,
         file_size: u64,
     },
-    /// A file type that is a read-only passthrough from a file on a remote serrver.
+    /// A file type that is a read-only passthrough from a file on a remote server.
     UnverifiedReadonly { reader: RemoteFileReader, file_size: u64 },
     /// A file type that is initially empty, and the content is stored on a remote server. File
     /// integrity is guaranteed with private Merkle tree.
@@ -84,17 +80,25 @@
 
     /// The next available inode number.
     next_inode: AtomicU64,
+
+    /// A reader to access the remote filesystem stats, which is supposed to be of "the" output
+    /// directory. We assume all output are stored in the same partition.
+    remote_fs_stats_reader: RemoteFsStatsReader,
 }
 
 // Implementation for preparing an `AuthFs` instance, before starting to serve.
 // TODO(victorhsieh): Consider implement a builder to separate the mutable initialization from the
 // immutable / interiorly mutable serving phase.
 impl AuthFs {
-    pub fn new() -> AuthFs {
+    pub fn new(remote_fs_stats_reader: RemoteFsStatsReader) -> AuthFs {
         let mut inode_table = BTreeMap::new();
         inode_table.insert(ROOT_INODE, AuthFsEntry::ReadonlyDirectory { dir: InMemoryDir::new() });
 
-        AuthFs { inode_table: Mutex::new(inode_table), next_inode: AtomicU64::new(ROOT_INODE + 1) }
+        AuthFs {
+            inode_table: Mutex::new(inode_table),
+            next_inode: AtomicU64::new(ROOT_INODE + 1),
+            remote_fs_stats_reader,
+        }
     }
 
     /// Add an `AuthFsEntry` as `basename` to the filesystem root.
@@ -206,13 +210,13 @@
         F: FnOnce(&mut AuthFsEntry, &Path, Inode) -> io::Result<AuthFsEntry>,
     {
         let mut inode_table = self.inode_table.lock().unwrap();
-        let mut parent_entry = inode_table
+        let parent_entry = inode_table
             .get_mut(&parent_inode)
             .ok_or_else(|| io::Error::from_raw_os_error(libc::ENOENT))?;
 
         let new_inode = self.next_inode.fetch_add(1, Ordering::Relaxed);
         let basename: &Path = cstr_to_path(name);
-        let new_file_entry = create_fn(&mut parent_entry, basename, new_inode)?;
+        let new_file_entry = create_fn(parent_entry, basename, new_inode)?;
         if let btree_map::Entry::Vacant(entry) = inode_table.entry(new_inode) {
             entry.insert(new_file_entry);
             Ok(new_inode)
@@ -672,36 +676,32 @@
             attr_timeout: DEFAULT_METADATA_TIMEOUT,
         })
     }
-}
 
-/// Mount and start the FUSE instance. This requires CAP_SYS_ADMIN.
-pub fn loop_forever(
-    authfs: AuthFs,
-    mountpoint: &Path,
-    extra_options: &Option<String>,
-) -> Result<(), fuse::Error> {
-    let dev_fuse = OpenOptions::new()
-        .read(true)
-        .write(true)
-        .open("/dev/fuse")
-        .expect("Failed to open /dev/fuse");
+    fn statfs(&self, _ctx: Context, _inode: Self::Inode) -> io::Result<libc::statvfs64> {
+        let remote_stat = self.remote_fs_stats_reader.statfs()?;
 
-    let mut mount_options = vec![
-        MountOption::FD(dev_fuse.as_raw_fd()),
-        MountOption::RootMode(libc::S_IFDIR | libc::S_IXUSR | libc::S_IXGRP | libc::S_IXOTH),
-        MountOption::AllowOther,
-        MountOption::UserId(0),
-        MountOption::GroupId(0),
-        MountOption::MaxRead(MAX_READ_BYTES),
-    ];
-    if let Some(value) = extra_options {
-        mount_options.push(MountOption::Extra(value));
+        // Safe because we are zero-initializing a struct with only POD fields. Not all fields
+        // matter to FUSE. See also:
+        // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/fs/fuse/inode.c?h=v5.15#n460
+        let mut st: libc::statvfs64 = unsafe { zeroed() };
+
+        // Use the remote stat as a template, since it'd matter the most to consider the writable
+        // files/directories that are written to the remote.
+        st.f_bsize = remote_stat.block_size;
+        st.f_frsize = remote_stat.fragment_size;
+        st.f_blocks = remote_stat.block_numbers;
+        st.f_bavail = remote_stat.block_available;
+        st.f_favail = remote_stat.inodes_available;
+        st.f_namemax = remote_stat.max_filename;
+        // Assuming we are not privileged to use all free spaces on the remote server, set the free
+        // blocks/fragment to the same available amount.
+        st.f_bfree = st.f_bavail;
+        st.f_ffree = st.f_favail;
+        // Number of inodes on the filesystem
+        st.f_files = self.inode_table.lock().unwrap().len() as u64;
+
+        Ok(st)
     }
-
-    fuse::mount(mountpoint, "authfs", libc::MS_NOSUID | libc::MS_NODEV, &mount_options)
-        .expect("Failed to mount fuse");
-
-    fuse::worker::start_message_loop(dev_fuse, MAX_WRITE_BYTES, MAX_READ_BYTES, authfs)
 }
 
 fn cstr_to_path(cstr: &CStr) -> &Path {
diff --git a/authfs/src/fusefs/mount.rs b/authfs/src/fusefs/mount.rs
new file mode 100644
index 0000000..e7f8c94
--- /dev/null
+++ b/authfs/src/fusefs/mount.rs
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+
+use fuse::mount::MountOption;
+use std::fs::OpenOptions;
+use std::os::unix::io::AsRawFd;
+use std::path::Path;
+
+use super::AuthFs;
+
+/// Maximum bytes in the write transaction to the FUSE device. This limits the maximum buffer
+/// size in a read request (including FUSE protocol overhead) that the filesystem writes to.
+pub const MAX_WRITE_BYTES: u32 = 65536;
+
+/// Maximum bytes in a read operation.
+/// TODO(victorhsieh): This option is deprecated by FUSE. Figure out if we can remove this.
+const MAX_READ_BYTES: u32 = 65536;
+
+/// Mount and start the FUSE instance to handle messages. This requires CAP_SYS_ADMIN.
+pub fn mount_and_enter_message_loop(
+    authfs: AuthFs,
+    mountpoint: &Path,
+    extra_options: &Option<String>,
+) -> Result<(), fuse::Error> {
+    let dev_fuse = OpenOptions::new()
+        .read(true)
+        .write(true)
+        .open("/dev/fuse")
+        .expect("Failed to open /dev/fuse");
+
+    let mut mount_options = vec![
+        MountOption::FD(dev_fuse.as_raw_fd()),
+        MountOption::RootMode(libc::S_IFDIR | libc::S_IXUSR | libc::S_IXGRP | libc::S_IXOTH),
+        MountOption::AllowOther,
+        MountOption::UserId(0),
+        MountOption::GroupId(0),
+        MountOption::MaxRead(MAX_READ_BYTES),
+    ];
+    if let Some(value) = extra_options {
+        mount_options.push(MountOption::Extra(value));
+    }
+
+    fuse::mount(mountpoint, "authfs", libc::MS_NOSUID | libc::MS_NODEV, &mount_options)
+        .expect("Failed to mount fuse");
+
+    fuse::worker::start_message_loop(dev_fuse, MAX_WRITE_BYTES, MAX_READ_BYTES, authfs)
+}
diff --git a/authfs/src/main.rs b/authfs/src/main.rs
index 0bc71ce..a083381 100644
--- a/authfs/src/main.rs
+++ b/authfs/src/main.rs
@@ -37,6 +37,7 @@
 mod common;
 mod crypto;
 mod file;
+mod fsstat;
 mod fsverity;
 mod fusefs;
 
@@ -44,6 +45,7 @@
 use file::{
     InMemoryDir, RemoteDirEditor, RemoteFileEditor, RemoteFileReader, RemoteMerkleTreeReader,
 };
+use fsstat::RemoteFsStatsReader;
 use fsverity::{VerifiedFileEditor, VerifiedFileReader};
 use fusefs::{AuthFs, AuthFsEntry};
 
@@ -204,9 +206,11 @@
     Ok(AuthFsEntry::VerifiedNewDirectory { dir })
 }
 
-fn prepare_root_dir_entries(authfs: &mut AuthFs, args: &Args) -> Result<()> {
-    let service = file::get_rpc_binder_service(args.cid)?;
-
+fn prepare_root_dir_entries(
+    service: file::VirtFdService,
+    authfs: &mut AuthFs,
+    args: &Args,
+) -> Result<()> {
     for config in &args.remote_ro_file {
         authfs.add_entry_at_root_dir(
             remote_fd_to_path_buf(config.remote_fd),
@@ -303,9 +307,11 @@
         android_logger::Config::default().with_tag("authfs").with_min_level(log_level),
     );
 
-    let mut authfs = AuthFs::new();
-    prepare_root_dir_entries(&mut authfs, &args)?;
-    fusefs::loop_forever(authfs, &args.mount_point, &args.extra_options)?;
+    let service = file::get_rpc_binder_service(args.cid)?;
+    let mut authfs = AuthFs::new(RemoteFsStatsReader::new(service.clone()));
+    prepare_root_dir_entries(service, &mut authfs, &args)?;
+
+    fusefs::mount_and_enter_message_loop(authfs, &args.mount_point, &args.extra_options)?;
     bail!("Unexpected exit after the handler loop")
 }
 
diff --git a/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java b/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
index c27c5cd..2d7668a 100644
--- a/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
+++ b/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
@@ -439,6 +439,18 @@
         assertFailedOnMicrodroid("test -f " + authfsInputDir + "/system/bin/sh");
     }
 
+    @Test
+    public void testStatfs() throws Exception {
+        // Setup
+        runFdServerOnAndroid("--open-dir 3:" + TEST_OUTPUT_DIR, "--rw-dirs 3");
+        runAuthFsOnMicrodroid("--remote-new-rw-dir 3 --cid " + VMADDR_CID_HOST);
+
+        // Verify
+        // Magic matches. Has only 2 inodes (root and "/3").
+        assertEquals(
+                FUSE_SUPER_MAGIC_HEX + " 2", runOnMicrodroid("stat -f -c '%t %c' " + MOUNT_DIR));
+    }
+
     private void expectBackingFileConsistency(
             String authFsPath, String backendPath, String expectedHash)
             throws DeviceNotAvailableException {
diff --git a/compos/apex/Android.bp b/compos/apex/Android.bp
index 43e75e4..f40da9c 100644
--- a/compos/apex/Android.bp
+++ b/compos/apex/Android.bp
@@ -35,6 +35,7 @@
 
     // TODO(victorhsieh): make it updatable
     updatable: false,
+    future_updatable: true,
     platform_apis: true,
 
     system_ext_specific: true,
diff --git a/compos/apk/assets/vm_config_staged.json b/compos/apk/assets/vm_config_staged.json
new file mode 100644
index 0000000..9c81e4e
--- /dev/null
+++ b/compos/apk/assets/vm_config_staged.json
@@ -0,0 +1,31 @@
+{
+  "version": 1,
+  "os": {
+    "name": "microdroid"
+  },
+  "task": {
+    "type": "executable",
+    "command": "/apex/com.android.compos/bin/compsvc",
+    "args": [
+      "--log_to_stderr"
+    ]
+  },
+  "prefer_staged": true,
+  "apexes": [
+    {
+      "name": "com.android.art"
+    },
+    {
+      "name": "com.android.compos"
+    },
+    {
+      "name": "{DEX2OATBOOTCLASSPATH}"
+    },
+    {
+      "name": "{BOOTCLASSPATH}"
+    },
+    {
+      "name": "{SYSTEMSERVERCLASSPATH}"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/compos/common/compos_client.rs b/compos/common/compos_client.rs
index 4908f94..6c16bb0 100644
--- a/compos/common/compos_client.rs
+++ b/compos/common/compos_client.rs
@@ -17,7 +17,7 @@
 //! Support for starting CompOS in a VM and connecting to the service
 
 use crate::timeouts::timeouts;
-use crate::{COMPOS_APEX_ROOT, COMPOS_DATA_ROOT, COMPOS_VSOCK_PORT};
+use crate::{COMPOS_APEX_ROOT, COMPOS_DATA_ROOT, COMPOS_VSOCK_PORT, DEFAULT_VM_CONFIG_PATH};
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
     IVirtualMachine::IVirtualMachine,
     IVirtualMachineCallback::{BnVirtualMachineCallback, IVirtualMachineCallback},
@@ -56,6 +56,8 @@
 pub struct VmParameters {
     /// Whether the VM should be debuggable.
     pub debug_mode: bool,
+    /// If present, overrides the path to the VM config JSON file
+    pub config_path: Option<String>,
 }
 
 impl VmInstance {
@@ -95,11 +97,12 @@
             (None, DebugLevel::NONE)
         };
 
+        let config_path = parameters.config_path.as_deref().unwrap_or(DEFAULT_VM_CONFIG_PATH);
         let config = VirtualMachineConfig::AppConfig(VirtualMachineAppConfig {
             apk: Some(apk_fd),
             idsig: Some(idsig_fd),
             instanceImage: Some(instance_fd),
-            configPath: "assets/vm_config.json".to_owned(),
+            configPath: config_path.to_owned(),
             debugLevel: debug_level,
             ..Default::default()
         });
@@ -199,18 +202,12 @@
     }
 }
 
-#[derive(Debug)]
+#[derive(Debug, Default)]
 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>,
diff --git a/compos/common/lib.rs b/compos/common/lib.rs
index 4bfa81f..9b07030 100644
--- a/compos/common/lib.rs
+++ b/compos/common/lib.rs
@@ -52,3 +52,10 @@
 
 /// The file that holds the instance image for a CompOS instance.
 pub const INSTANCE_IMAGE_FILE: &str = "instance.img";
+
+/// The path within our config APK of our default VM configuration file, used at boot time.
+pub const DEFAULT_VM_CONFIG_PATH: &str = "assets/vm_config.json";
+
+/// The path within our config APK of the VM configuration file we use when compiling staged
+/// APEXes before reboot.
+pub const PREFER_STAGED_VM_CONFIG_PATH: &str = "assets/vm_config_staged.json";
diff --git a/compos/compos_key_cmd/compos_key_cmd.cpp b/compos/compos_key_cmd/compos_key_cmd.cpp
index c2699ab..27c7275 100644
--- a/compos/compos_key_cmd/compos_key_cmd.cpp
+++ b/compos/compos_key_cmd/compos_key_cmd.cpp
@@ -74,8 +74,9 @@
 constexpr const char* kConfigApkIdsigPath =
         "/apex/com.android.compos/etc/CompOSPayloadApp.apk.idsig";
 
-// This is a path inside the APK
-constexpr const char* kConfigFilePath = "assets/vm_config.json";
+// These are paths inside the APK
+constexpr const char* kDefaultConfigFilePath = "assets/vm_config.json";
+constexpr const char* kPreferStagedConfigFilePath = "assets/vm_config_staged.json";
 
 static bool writeBytesToFile(const std::vector<uint8_t>& bytes, const std::string& path) {
     std::string str(bytes.begin(), bytes.end());
@@ -101,6 +102,10 @@
 
 void copyToLog(unique_fd&& fd) {
     FILE* source = Fdopen(std::move(fd), "r");
+    if (source == nullptr) {
+        LOG(INFO) << "Can't log VM output";
+        return;
+    }
     size_t size = 0;
     char* line = nullptr;
 
@@ -147,9 +152,13 @@
 
     ::ndk::ScopedAStatus onError(int32_t in_cid, int32_t in_error_code,
                                  const std::string& in_message) override {
-        // For now, just log the error as onDied() will follow.
         LOG(WARNING) << "VM error! cid = " << in_cid << ", error_code = " << in_error_code
                      << ", message = " << in_message;
+        {
+            std::unique_lock lock(mMutex);
+            mDied = true;
+        }
+        mCv.notify_all();
         return ScopedAStatus::ok();
     }
 
@@ -182,11 +191,12 @@
 class TargetVm {
 public:
     TargetVm(int cid, const std::string& logFile, const std::string& instanceImageFile,
-             bool debuggable)
+             bool debuggable, bool preferStaged)
           : mCid(cid),
             mLogFile(logFile),
             mInstanceImageFile(instanceImageFile),
-            mDebuggable(debuggable) {}
+            mDebuggable(debuggable),
+            mPreferStaged(preferStaged) {}
 
     // Returns 0 if we are to connect to a local service, otherwise the CID of
     // either an existing VM or a VM we have started, depending on the command
@@ -237,7 +247,7 @@
         }
 
         ScopedFileDescriptor instanceFd(
-                TEMP_FAILURE_RETRY(open(mInstanceImageFile.c_str(), O_RDONLY | O_CLOEXEC)));
+                TEMP_FAILURE_RETRY(open(mInstanceImageFile.c_str(), O_RDWR | O_CLOEXEC)));
         if (instanceFd.get() == -1) {
             return ErrnoError() << "Failed to open instance image file";
         }
@@ -247,7 +257,7 @@
         appConfig.apk = std::move(apkFd);
         appConfig.idsig = std::move(idsigFd);
         appConfig.instanceImage = std::move(instanceFd);
-        appConfig.configPath = kConfigFilePath;
+        appConfig.configPath = mPreferStaged ? kPreferStagedConfigFilePath : kDefaultConfigFilePath;
         appConfig.debugLevel = mDebuggable ? VirtualMachineAppConfig::DebugLevel::FULL
                                            : VirtualMachineAppConfig::DebugLevel::NONE;
         appConfig.memoryMib = 0; // Use default
@@ -293,6 +303,7 @@
     const std::string mLogFile;
     const std::string mInstanceImageFile;
     const bool mDebuggable;
+    const bool mPreferStaged;
     std::shared_ptr<Callback> mCallback;
     std::shared_ptr<IVirtualMachine> mVm;
 };
@@ -539,17 +550,25 @@
     std::string imageFile;
     std::string logFile;
     bool debuggable = false;
+    bool preferStaged = false;
 
     for (;;) {
+        // Options with no associated value
         if (argc >= 2) {
             if (argv[1] == "--debug"sv) {
                 debuggable = true;
                 argc -= 1;
                 argv += 1;
                 continue;
+            } else if (argv[1] == "--staged"sv) {
+                preferStaged = true;
+                argc -= 1;
+                argv += 1;
+                continue;
             }
         }
         if (argc < 3) break;
+        // Options requiring a value
         if (argv[1] == "--cid"sv) {
             cid = atoi(argv[2]);
             if (cid == 0) {
@@ -567,7 +586,7 @@
         argv += 2;
     }
 
-    TargetVm vm(cid, logFile, imageFile, debuggable);
+    TargetVm vm(cid, logFile, imageFile, debuggable, preferStaged);
 
     if (argc == 4 && argv[1] == "generate"sv) {
         auto result = generate(vm, argv[2], argv[3]);
@@ -623,9 +642,11 @@
                   << "    <filename>.signature\n"
                   << "  make-instance <image file> Create an empty instance image file for a VM.\n"
                   << "\n"
-                  << "OPTIONS: --log <log file> --debug (--cid <cid> | --start <image file>)\n"
+                  << "OPTIONS: --log <log file> --debug --staged\n"
+                  << "    (--cid <cid> | --start <image file>)\n"
                   << "  Specify --log to write VM log to a file rather than stdout.\n"
                   << "  Specify --debug with --start to make the VM fully debuggable.\n"
+                  << "  Specify --staged with --start to prefer staged APEXes in the VM.\n"
                   << "  Specify --cid to connect to a VM rather than the host.\n"
                   << "  Specify --start to start a VM from the given instance image file and\n "
                   << "    connect to that.\n";
diff --git a/compos/composd/aidl/android/system/composd/IIsolatedCompilationService.aidl b/compos/composd/aidl/android/system/composd/IIsolatedCompilationService.aidl
index 6f4476c..5e72cd2 100644
--- a/compos/composd/aidl/android/system/composd/IIsolatedCompilationService.aidl
+++ b/compos/composd/aidl/android/system/composd/IIsolatedCompilationService.aidl
@@ -20,6 +20,17 @@
 
 interface IIsolatedCompilationService {
     /**
+     * Compile BCP extensions and system server, using any staged APEXes that are present in
+     * preference to active APEXes, writing the results to the pending artifacts directory to be
+     * verified by odsing on next boot.
+     *
+     * Compilation continues in the background, and success/failure is reported via the supplied
+     * callback, unless the returned ICompilationTask is cancelled. The caller should maintain
+     * a reference to the ICompilationTask until compilation completes or is cancelled.
+     */
+    ICompilationTask startStagedApexCompile(ICompilationTaskCallback callback);
+
+    /**
      * Run "odrefresh --dalvik-cache=pending-test --force-compile" in a test instance of CompOS.
      *
      * This compiles BCP extensions and system server, even if the system artifacts are up to date,
diff --git a/compos/composd/native/Android.bp b/compos/composd/native/Android.bp
index ad0afd9..135f4d4 100644
--- a/compos/composd/native/Android.bp
+++ b/compos/composd/native/Android.bp
@@ -7,12 +7,17 @@
     crate_name: "composd_native",
     srcs: ["lib.rs"],
     rustlibs: [
+        "libanyhow",
         "libcxx",
+        "liblibc",
     ],
     static_libs: [
         "libcomposd_native_cpp",
     ],
-    shared_libs: ["libcrypto"],
+    shared_libs: [
+        "libartpalette-system",
+        "libcrypto",
+    ],
     apex_available: ["com.android.compos"],
 }
 
diff --git a/compos/composd/native/lib.rs b/compos/composd/native/lib.rs
index ace9600..cbec7fd 100644
--- a/compos/composd/native/lib.rs
+++ b/compos/composd/native/lib.rs
@@ -12,12 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-//! Bindings native helpers for composd.
+//! Native helpers for composd.
 
-pub use ffi::*;
+pub use art::*;
+pub use crypto::*;
 
 #[cxx::bridge]
-mod ffi {
+mod crypto {
     /// Contains either a key or a reason why the key could not be extracted.
     struct KeyResult {
         /// The extracted key. If empty, the attempt to extract the key failed.
@@ -36,3 +37,38 @@
         fn extract_rsa_public_key(der_certificate: &[u8]) -> KeyResult;
     }
 }
+
+mod art {
+    use anyhow::{anyhow, Result};
+    use libc::c_char;
+    use std::ffi::{CStr, OsStr};
+    use std::io::Error;
+    use std::os::unix::ffi::OsStrExt;
+    use std::path::Path;
+    use std::ptr::null;
+
+    // From libartpalette(-system)
+    extern "C" {
+        fn PaletteCreateOdrefreshStagingDirectory(out_staging_dir: *mut *const c_char) -> i32;
+    }
+    const PALETTE_STATUS_OK: i32 = 0;
+    const PALETTE_STATUS_CHECK_ERRNO: i32 = 1;
+
+    /// Creates and returns the staging directory for odrefresh.
+    pub fn palette_create_odrefresh_staging_directory() -> Result<&'static Path> {
+        let mut staging_dir: *const c_char = null();
+        // SAFETY: The C function always returns a non-null C string (after created the directory).
+        let status = unsafe { PaletteCreateOdrefreshStagingDirectory(&mut staging_dir) };
+        match status {
+            PALETTE_STATUS_OK => {
+                // SAFETY: The previously returned `*const c_char` should point to a legitimate C
+                // string.
+                let cstr = unsafe { CStr::from_ptr(staging_dir) };
+                let path = OsStr::from_bytes(cstr.to_bytes()).as_ref();
+                Ok(path)
+            }
+            PALETTE_STATUS_CHECK_ERRNO => Err(anyhow!(Error::last_os_error().to_string())),
+            _ => Err(anyhow!("Failed with palette status {}", status)),
+        }
+    }
+}
diff --git a/compos/composd/src/compilation_task.rs b/compos/composd/src/compilation_task.rs
index c4eed52..18f5aac 100644
--- a/compos/composd/src/compilation_task.rs
+++ b/compos/composd/src/compilation_task.rs
@@ -52,6 +52,23 @@
         self.running_task.lock().unwrap().take()
     }
 
+    pub fn start_staged_apex_compile(
+        comp_os: Arc<CompOsInstance>,
+        callback: &Strong<dyn ICompilationTaskCallback>,
+    ) -> Result<CompilationTask> {
+        // TODO: Write to pending
+        // TODO: Delete any existing artifacts
+        let odrefresh = Odrefresh::spawn_compile("test-artifacts")?;
+        let odrefresh = Arc::new(odrefresh);
+        let task =
+            RunningTask { odrefresh: odrefresh.clone(), comp_os, callback: callback.clone() };
+        let task = CompilationTask { running_task: Arc::new(Mutex::new(Some(task))) };
+
+        task.clone().start_waiting_thread(odrefresh);
+
+        Ok(task)
+    }
+
     pub fn start_test_compile(
         comp_os: Arc<CompOsInstance>,
         callback: &Strong<dyn ICompilationTaskCallback>,
diff --git a/compos/composd/src/instance_manager.rs b/compos/composd/src/instance_manager.rs
index 6291d59..24ae576 100644
--- a/compos/composd/src/instance_manager.rs
+++ b/compos/composd/src/instance_manager.rs
@@ -23,7 +23,7 @@
 use compos_aidl_interface::aidl::com::android::compos::ICompOsService::ICompOsService;
 use compos_aidl_interface::binder::Strong;
 use compos_common::compos_client::VmParameters;
-use compos_common::{CURRENT_INSTANCE_DIR, TEST_INSTANCE_DIR};
+use compos_common::{PENDING_INSTANCE_DIR, PREFER_STAGED_VM_CONFIG_PATH, TEST_INSTANCE_DIR};
 use std::sync::{Arc, Mutex, Weak};
 use virtualizationservice::IVirtualizationService::IVirtualizationService;
 
@@ -43,13 +43,14 @@
         Ok(instance.get_service())
     }
 
-    #[allow(dead_code)] // TODO: Make use of this
-    pub fn start_current_instance(&self) -> Result<Arc<CompOsInstance>> {
-        self.start_instance(CURRENT_INSTANCE_DIR, VmParameters::default())
+    pub fn start_pending_instance(&self) -> Result<Arc<CompOsInstance>> {
+        let config_path = Some(PREFER_STAGED_VM_CONFIG_PATH.to_owned());
+        let vm_parameters = VmParameters { config_path, ..Default::default() };
+        self.start_instance(PENDING_INSTANCE_DIR, vm_parameters)
     }
 
     pub fn start_test_instance(&self) -> Result<Arc<CompOsInstance>> {
-        let vm_parameters = VmParameters { debug_mode: true };
+        let vm_parameters = VmParameters { debug_mode: true, ..Default::default() };
         self.start_instance(TEST_INSTANCE_DIR, vm_parameters)
     }
 
diff --git a/compos/composd/src/instance_starter.rs b/compos/composd/src/instance_starter.rs
index 3e18c3e..8189fe0 100644
--- a/compos/composd/src/instance_starter.rs
+++ b/compos/composd/src/instance_starter.rs
@@ -199,7 +199,7 @@
 
     fn check_files_exist(&self) -> Result<()> {
         if !self.instance_root.is_dir() {
-            bail!("Directory {} not found", self.instance_root.display())
+            bail!("Directory {:?} not found", self.instance_root)
         };
         Self::check_file_exists(&self.instance_image)?;
         Self::check_file_exists(&self.key_blob)?;
@@ -209,7 +209,7 @@
 
     fn check_file_exists(file: &Path) -> Result<()> {
         if !file.is_file() {
-            bail!("File {} not found", file.display())
+            bail!("File {:?} not found", file)
         };
         Ok(())
     }
diff --git a/compos/composd/src/odrefresh.rs b/compos/composd/src/odrefresh.rs
index 16dcb0f..9debf00 100644
--- a/compos/composd/src/odrefresh.rs
+++ b/compos/composd/src/odrefresh.rs
@@ -44,7 +44,15 @@
 }
 
 impl Odrefresh {
+    pub fn spawn_compile(target_dir: &str) -> Result<Self> {
+        Self::spawn_odrefresh(target_dir, "--compile")
+    }
+
     pub fn spawn_forced_compile(target_dir: &str) -> Result<Self> {
+        Self::spawn_odrefresh(target_dir, "--force-compile")
+    }
+
+    fn spawn_odrefresh(target_dir: &str, compile_arg: &str) -> Result<Self> {
         // We don`t need to capture stdout/stderr - odrefresh writes to the log
         let mut cmdline = Command::new(ODREFRESH_BIN);
         if need_extra_time()? {
@@ -61,7 +69,7 @@
         cmdline
             .arg(format!("--use-compilation-os={}", VMADDR_CID_ANY as i32))
             .arg(format!("--dalvik-cache={}", target_dir))
-            .arg("--force-compile");
+            .arg(compile_arg);
         let child = SharedChild::spawn(&mut cmdline).context("Running odrefresh")?;
         Ok(Odrefresh { child })
     }
diff --git a/compos/composd/src/service.rs b/compos/composd/src/service.rs
index 3738e18..a2898a2 100644
--- a/compos/composd/src/service.rs
+++ b/compos/composd/src/service.rs
@@ -20,7 +20,6 @@
 use crate::compilation_task::CompilationTask;
 use crate::fd_server_helper::FdServerConfig;
 use crate::instance_manager::InstanceManager;
-use crate::instance_starter::CompOsInstance;
 use crate::util::to_binder_result;
 use android_system_composd::aidl::android::system::composd::{
     ICompilationTask::{BnCompilationTask, ICompilationTask},
@@ -31,12 +30,11 @@
     self, BinderFeatures, ExceptionCode, Interface, Status, Strong, ThreadState,
 };
 use anyhow::{Context, Result};
-use compos_common::COMPOS_DATA_ROOT;
 use rustutils::{system_properties, users::AID_ROOT, users::AID_SYSTEM};
-use std::fs::{create_dir, File, OpenOptions};
+use std::fs::{File, OpenOptions};
 use std::os::unix::fs::OpenOptionsExt;
 use std::os::unix::io::AsRawFd;
-use std::path::{Path, PathBuf};
+use std::path::Path;
 use std::sync::Arc;
 
 pub struct IsolatedCompilationService {
@@ -53,21 +51,41 @@
 impl Interface for IsolatedCompilationService {}
 
 impl IIsolatedCompilationService for IsolatedCompilationService {
+    fn startStagedApexCompile(
+        &self,
+        callback: &Strong<dyn ICompilationTaskCallback>,
+    ) -> binder::Result<Strong<dyn ICompilationTask>> {
+        check_permissions()?;
+        to_binder_result(self.do_start_staged_apex_compile(callback))
+    }
+
     fn startTestCompile(
         &self,
         callback: &Strong<dyn ICompilationTaskCallback>,
     ) -> binder::Result<Strong<dyn ICompilationTask>> {
-        check_test_permissions()?;
+        check_permissions()?;
         to_binder_result(self.do_start_test_compile(callback))
     }
 
     fn startTestOdrefresh(&self) -> binder::Result<i8> {
-        check_test_permissions()?;
+        check_permissions()?;
         to_binder_result(self.do_odrefresh_for_test())
     }
 }
 
 impl IsolatedCompilationService {
+    fn do_start_staged_apex_compile(
+        &self,
+        callback: &Strong<dyn ICompilationTaskCallback>,
+    ) -> Result<Strong<dyn ICompilationTask>> {
+        // TODO: Try to start the current instance with staged APEXes to see if it works?
+        let comp_os = self.instance_manager.start_pending_instance().context("Starting CompOS")?;
+
+        let task = CompilationTask::start_staged_apex_compile(comp_os, callback)?;
+
+        Ok(BnCompilationTask::new_binder(task, BinderFeatures::default()))
+    }
+
     fn do_start_test_compile(
         &self,
         callback: &Strong<dyn ICompilationTaskCallback>,
@@ -80,20 +98,13 @@
     }
 
     fn do_odrefresh_for_test(&self) -> Result<i8> {
-        let mut staging_dir_path = PathBuf::from(COMPOS_DATA_ROOT);
-        staging_dir_path.push("test-artifacts");
-        to_binder_result(create_dir(&staging_dir_path))?;
-
         let compos = self
             .instance_manager
             .start_test_instance()
             .context("Starting CompOS for odrefresh test")?;
-        self.do_odrefresh(compos, &staging_dir_path)
-    }
 
-    fn do_odrefresh(&self, compos: Arc<CompOsInstance>, staging_dir_path: &Path) -> Result<i8> {
-        let output_dir = open_dir_path(staging_dir_path)?;
-        let system_dir = open_dir_path(Path::new("/system"))?;
+        let output_dir = open_dir(composd_native::palette_create_odrefresh_staging_directory()?)?;
+        let system_dir = open_dir(Path::new("/system"))?;
 
         // Spawn a fd_server to serve the FDs.
         let fd_server_config = FdServerConfig {
@@ -114,7 +125,7 @@
     }
 }
 
-fn check_test_permissions() -> binder::Result<()> {
+fn check_permissions() -> binder::Result<()> {
     let calling_uid = ThreadState::get_calling_uid();
     // This should only be called by system server, or root while testing
     if calling_uid != AID_SYSTEM && calling_uid != AID_ROOT {
@@ -124,16 +135,12 @@
     }
 }
 
-/// Returns an owned FD of the directory path. It currently returns a `File` as a FD owner, but
+/// Returns an owned FD of the directory. It currently returns a `File` as a FD owner, but
 /// it's better to use `std::os::unix::io::OwnedFd` once/if it becomes standard.
-fn open_dir_path(path: &Path) -> Result<File> {
+fn open_dir(path: &Path) -> Result<File> {
     OpenOptions::new()
-        .custom_flags(libc::O_PATH | libc::O_DIRECTORY)
-        // The custom flags above is not taken into consideration by the unix implementation of
-        // OpenOptions for flag validation. So even though the man page of open(2) says that
-        // most flags include access mode are ignored, we still need to set a "valid" mode to
-        // make the library happy. The value does not appear to matter elsewhere in the library.
-        .read(true)
+        .custom_flags(libc::O_DIRECTORY)
+        .read(true) // O_DIRECTORY can only be opened with read
         .open(path)
-        .with_context(|| format!("Failed to open {} directory as path fd", path.display()))
+        .with_context(|| format!("Failed to open {:?} directory as path fd", path))
 }
diff --git a/compos/composd_cmd/composd_cmd.rs b/compos/composd_cmd/composd_cmd.rs
index e591794..f22dc13 100644
--- a/compos/composd_cmd/composd_cmd.rs
+++ b/compos/composd_cmd/composd_cmd.rs
@@ -18,12 +18,13 @@
 
 use android_system_composd::{
     aidl::android::system::composd::{
+        ICompilationTask::ICompilationTask,
         ICompilationTaskCallback::{BnCompilationTaskCallback, ICompilationTaskCallback},
         IIsolatedCompilationService::IIsolatedCompilationService,
     },
     binder::{
         wait_for_interface, BinderFeatures, DeathRecipient, IBinder, Interface, ProcessState,
-        Result as BinderResult,
+        Result as BinderResult, Strong,
     },
 };
 use anyhow::{bail, Context, Result};
@@ -37,7 +38,7 @@
             .index(1)
             .takes_value(true)
             .required(true)
-            .possible_values(&["forced-compile-test", "forced-odrefresh"]),
+            .possible_values(&["staged-apex-compile", "forced-compile-test", "forced-odrefresh"]),
     );
     let args = app.get_matches();
     let command = args.value_of("command").unwrap();
@@ -45,6 +46,7 @@
     ProcessState::start_thread_pool();
 
     match command {
+        "staged-apex-compile" => run_staged_apex_compile()?,
         "forced-compile-test" => run_forced_compile_for_test()?,
         "forced-odrefresh" => run_forced_odrefresh_for_test()?,
         _ => panic!("Unexpected command {}", command),
@@ -103,14 +105,28 @@
     }
 }
 
+fn run_staged_apex_compile() -> Result<()> {
+    run_async_compilation(|service, callback| service.startStagedApexCompile(callback))
+}
+
 fn run_forced_compile_for_test() -> Result<()> {
+    run_async_compilation(|service, callback| service.startTestCompile(callback))
+}
+
+fn run_async_compilation<F>(start_compile_fn: F) -> Result<()>
+where
+    F: FnOnce(
+        &dyn IIsolatedCompilationService,
+        &Strong<dyn ICompilationTaskCallback>,
+    ) -> BinderResult<Strong<dyn ICompilationTask>>,
+{
     let service = wait_for_interface::<dyn IIsolatedCompilationService>("android.system.composd")
         .context("Failed to connect to composd service")?;
 
     let state = Arc::new(State::default());
     let callback = Callback(state.clone());
     let callback = BnCompilationTaskCallback::new_binder(callback, BinderFeatures::default());
-    let task = service.startTestCompile(&callback).context("Compilation failed")?;
+    let task = start_compile_fn(&*service, &callback).context("Compilation failed")?;
 
     // Make sure composd keeps going even if we don't hold a reference to its service.
     drop(service);
diff --git a/compos/service/Android.bp b/compos/service/Android.bp
index 6270c9a..336ae9b 100644
--- a/compos/service/Android.bp
+++ b/compos/service/Android.bp
@@ -12,6 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
 java_library {
     name: "service-compos",
     srcs: [
diff --git a/compos/service/java/com/android/server/compos/IsolatedCompilationJobService.java b/compos/service/java/com/android/server/compos/IsolatedCompilationJobService.java
index 2aacc2d..685d60c 100644
--- a/compos/service/java/com/android/server/compos/IsolatedCompilationJobService.java
+++ b/compos/service/java/com/android/server/compos/IsolatedCompilationJobService.java
@@ -68,20 +68,21 @@
         CompilationJob newJob = new CompilationJob(callback);
         mCurrentJob.set(newJob);
 
-        try {
-            // This can take some time - we need to start up a VM - so we do it on a separate
-            // thread. This thread exits as soon as the compilation Ttsk has been started (or
-            // there's a failure), and then compilation continues in composd and the VM.
-            new Thread("IsolatedCompilationJob_starter") {
-                @Override
-                public void run() {
+        // This can take some time - we need to start up a VM - so we do it on a separate
+        // thread. This thread exits as soon as the compilation Task has been started (or
+        // there's a failure), and then compilation continues in composd and the VM.
+        new Thread("IsolatedCompilationJob_starter") {
+            @Override
+            public void run() {
+                try {
                     newJob.start();
+                } catch (RuntimeException e) {
+                    Log.e(TAG, "Starting CompilationJob failed", e);
+                    newJob.stop(); // Just in case it managed to start before failure
+                    jobFinished(params, /*wantReschedule=*/ false);
                 }
-            }.start();
-        } catch (RuntimeException e) {
-            Log.e(TAG, "Starting CompilationJob failed", e);
-            return false; // We're finished
-        }
+            }
+        }.start();
         return true; // Job is running in the background
     }
 
@@ -137,6 +138,7 @@
             }
 
             try {
+                // TODO(b/205296305) Call startStagedApexCompile instead
                 ICompilationTask composTask = composd.startTestCompile(this);
                 mTask.set(composTask);
                 composTask.asBinder().linkToDeath(this, 0);
diff --git a/compos/src/compilation.rs b/compos/src/compilation.rs
index b726a1e..44b4049 100644
--- a/compos/src/compilation.rs
+++ b/compos/src/compilation.rs
@@ -86,14 +86,20 @@
     android_root.push("system");
     env::set_var("ANDROID_ROOT", &android_root);
 
+    let mut art_apex_data = mountpoint.clone();
+    art_apex_data.push(output_dir_fd.to_string());
+    env::set_var("ART_APEX_DATA", &art_apex_data);
+
     let mut staging_dir = mountpoint;
     staging_dir.push(output_dir_fd.to_string());
     staging_dir.push("staging");
-    create_dir(&staging_dir).context("Create staging directory")?;
+    create_dir(&staging_dir)
+        .with_context(|| format!("Create staging directory {}", staging_dir.display()))?;
 
     let args = vec![
         "odrefresh".to_string(),
         format!("--zygote-arch={}", zygote_arch),
+        "--no-refresh".to_string(),
         format!("--staging-dir={}", staging_dir.display()),
         "--force-compile".to_string(),
     ];
@@ -103,7 +109,7 @@
         // TODO(161471326): On success, sign all files in the output directory.
         Ok(()) => Ok(CompilerOutput::ExitCode(0)),
         Err(minijail::Error::ReturnCode(exit_code)) => {
-            error!("dex2oat failed with exit code {}", exit_code);
+            error!("odrefresh failed with exit code {}", exit_code);
             Ok(CompilerOutput::ExitCode(exit_code as i8))
         }
         Err(e) => {
diff --git a/compos/verify_key/verify_key.rs b/compos/verify_key/verify_key.rs
index 945acb4..e0ed5e5 100644
--- a/compos/verify_key/verify_key.rs
+++ b/compos/verify_key/verify_key.rs
@@ -105,8 +105,11 @@
     let instance_image = File::open(instance_image).context("Failed to open instance image")?;
 
     let virtualization_service = VmInstance::connect_to_virtualization_service()?;
-    let vm_instance =
-        VmInstance::start(&*virtualization_service, instance_image, &VmParameters { debug_mode })?;
+    let vm_instance = VmInstance::start(
+        &*virtualization_service,
+        instance_image,
+        &VmParameters { debug_mode, ..Default::default() },
+    )?;
     let service = vm_instance.get_service()?;
 
     let result = service.verifySigningKey(&blob, &public_key).context("Verifying signing key")?;
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java b/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java
index 9dbed64..2ddaf30 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java
@@ -33,7 +33,12 @@
 public interface VirtualMachineCallback {
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
-    @IntDef({ERROR_UNKNOWN, ERROR_PAYLOAD_VERIFICATION_FAILED, ERROR_PAYLOAD_CHANGED})
+    @IntDef({
+        ERROR_UNKNOWN,
+        ERROR_PAYLOAD_VERIFICATION_FAILED,
+        ERROR_PAYLOAD_CHANGED,
+        ERROR_PAYLOAD_INVALID_CONFIG
+    })
     @interface ErrorCode {}
 
     /** Error code for all other errors not listed below. */
@@ -48,6 +53,9 @@
     /** Error code indicating that the payload is verified, but has changed since the last boot. */
     int ERROR_PAYLOAD_CHANGED = 2;
 
+    /** Error code indicating that the payload config is invalid. */
+    int ERROR_PAYLOAD_INVALID_CONFIG = 3;
+
     /** Called when the payload starts in the VM. */
     void onPayloadStarted(@NonNull VirtualMachine vm, @Nullable ParcelFileDescriptor stream);
 
diff --git a/microdroid_manager/src/main.rs b/microdroid_manager/src/main.rs
index 2e6fa36..93a0759 100644
--- a/microdroid_manager/src/main.rs
+++ b/microdroid_manager/src/main.rs
@@ -33,17 +33,26 @@
 use std::fs::{self, File, OpenOptions};
 use std::os::unix::io::{FromRawFd, IntoRawFd};
 use std::path::Path;
-use std::process::{Command, Stdio};
+use std::process::{Child, Command, Stdio};
 use std::str;
 use std::time::{Duration, SystemTime};
 use vsock::VsockStream;
 
 use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice::IVirtualMachineService::{
-    ERROR_PAYLOAD_CHANGED, ERROR_PAYLOAD_VERIFICATION_FAILED, ERROR_UNKNOWN, VM_BINDER_SERVICE_PORT, VM_STREAM_SERVICE_PORT, IVirtualMachineService,
+    ERROR_PAYLOAD_CHANGED, ERROR_PAYLOAD_VERIFICATION_FAILED, ERROR_PAYLOAD_INVALID_CONFIG, ERROR_UNKNOWN, VM_BINDER_SERVICE_PORT, VM_STREAM_SERVICE_PORT, IVirtualMachineService,
 };
 
 const WAIT_TIMEOUT: Duration = Duration::from_secs(10);
+const APK_DM_VERITY_ARGUMENT: ApkDmverityArgument = {
+    ApkDmverityArgument {
+        apk: "/dev/block/by-name/microdroid-apk",
+        idsig: "/dev/block/by-name/microdroid-apk-idsig",
+        name: "microdroid-apk",
+    }
+};
 const DM_MOUNTED_APK_PATH: &str = "/dev/block/mapper/microdroid-apk";
+const APKDMVERITY_BIN: &str = "/system/bin/apkdmverity";
+const ZIPFUSE_BIN: &str = "/system/bin/zipfuse";
 
 /// The CID representing the host VM
 const VMADDR_CID_HOST: u32 = 2;
@@ -57,6 +66,8 @@
     PayloadChanged(String),
     #[error("Payload verification has failed: {0}")]
     PayloadVerificationFailed(String),
+    #[error("Payload config is invalid: {0}")]
+    InvalidConfig(String),
 }
 
 fn translate_error(err: &Error) -> (i32, String) {
@@ -66,6 +77,7 @@
             MicrodroidError::PayloadVerificationFailed(msg) => {
                 (ERROR_PAYLOAD_VERIFICATION_FAILED, msg.to_string())
             }
+            MicrodroidError::InvalidConfig(msg) => (ERROR_PAYLOAD_INVALID_CONFIG, msg.to_string()),
         }
     } else {
         (ERROR_UNKNOWN, err.to_string())
@@ -103,16 +115,27 @@
     info!("started.");
 
     let service = get_vms_rpc_binder().context("cannot connect to VirtualMachineService")?;
-    if let Err(err) = try_start_payload(&service) {
-        let (error_code, message) = translate_error(&err);
-        service.notifyError(error_code, &message)?;
-        Err(err)
-    } else {
-        Ok(())
+    match try_run_payload(&service) {
+        Ok(code) => {
+            info!("notifying payload finished");
+            service.notifyPayloadFinished(code)?;
+            if code == 0 {
+                info!("task successfully finished");
+            } else {
+                error!("task exited with exit code: {}", code);
+            }
+            Ok(())
+        }
+        Err(err) => {
+            error!("task terminated: {:?}", err);
+            let (error_code, message) = translate_error(&err);
+            service.notifyError(error_code, &message)?;
+            Err(err)
+        }
     }
 }
 
-fn try_start_payload(service: &Strong<dyn IVirtualMachineService>) -> Result<()> {
+fn try_run_payload(service: &Strong<dyn IVirtualMachineService>) -> Result<i32> {
     let metadata = load_metadata().context("Failed to load payload metadata")?;
 
     let mut instance = InstanceDisk::new().context("Failed to load instance.img")?;
@@ -135,29 +158,64 @@
     }
 
     // Before reading a file from the APK, start zipfuse
-    system_properties::write("ctl.start", "zipfuse")?;
+    run_zipfuse(
+        "fscontext=u:object_r:zipfusefs:s0,context=u:object_r:system_file:s0",
+        Path::new("/dev/block/mapper/microdroid-apk"),
+        Path::new("/mnt/apk"),
+    )
+    .context("Failed to run zipfuse")?;
 
-    if !metadata.payload_config_path.is_empty() {
-        let config = load_config(Path::new(&metadata.payload_config_path))?;
+    ensure!(
+        !metadata.payload_config_path.is_empty(),
+        MicrodroidError::InvalidConfig("No payload_config_path in metadata".to_string())
+    );
+    let config = load_config(Path::new(&metadata.payload_config_path))?;
 
-        let fake_secret = "This is a placeholder for a value that is derived from the images that are loaded in the VM.";
-        if let Err(err) = rustutils::system_properties::write("ro.vmsecret.keymint", fake_secret) {
-            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);
-                e
-            })?;
-        }
+    let fake_secret = "This is a placeholder for a value that is derived from the images that are loaded in the VM.";
+    if let Err(err) = rustutils::system_properties::write("ro.vmsecret.keymint", fake_secret) {
+        warn!("failed to set ro.vmsecret.keymint: {}", err);
     }
 
-    Ok(())
+    // Wait until apex config is done. (e.g. linker configuration for apexes)
+    // TODO(jooyung): wait until sys.boot_completed?
+    wait_for_apex_config_done()?;
+
+    ensure!(
+        config.task.is_some(),
+        MicrodroidError::InvalidConfig("No task in VM config".to_string())
+    );
+    exec_task(&config.task.unwrap(), service)
+}
+
+struct ApkDmverityArgument<'a> {
+    apk: &'a str,
+    idsig: &'a str,
+    name: &'a str,
+}
+
+fn run_apkdmverity(args: &[ApkDmverityArgument]) -> Result<Child> {
+    let mut cmd = Command::new(APKDMVERITY_BIN);
+
+    cmd.stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null());
+
+    for argument in args {
+        cmd.arg("--apk").arg(argument.apk).arg(argument.idsig).arg(argument.name);
+    }
+
+    cmd.spawn().context("Spawn apkdmverity")
+}
+
+fn run_zipfuse(option: &str, zip_path: &Path, mount_dir: &Path) -> Result<Child> {
+    Command::new(ZIPFUSE_BIN)
+        .arg("-o")
+        .arg(option)
+        .arg(zip_path)
+        .arg(mount_dir)
+        .stdin(Stdio::null())
+        .stdout(Stdio::null())
+        .stderr(Stdio::null())
+        .spawn()
+        .context("Spawn zipfuse")
 }
 
 // Verify payload before executing it. For APK payload, Full verification (which is slow) is done
@@ -182,7 +240,7 @@
     }
 
     // Start apkdmverity and wait for the dm-verify block
-    system_properties::write("ctl.start", "apkdmverity")?;
+    let mut apkdmverity_child = run_apkdmverity(&[APK_DM_VERITY_ARGUMENT])?;
 
     // While waiting for apkdmverity to mount APK, gathers public keys and root digests from
     // APEX payload.
@@ -206,7 +264,8 @@
     // Start apexd to activate APEXes
     system_properties::write("ctl.start", "apexd-vm")?;
 
-    ioutil::wait_for_file(DM_MOUNTED_APK_PATH, WAIT_TIMEOUT)?;
+    // TODO(inseob): add timeout
+    apkdmverity_child.wait()?;
 
     // 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
@@ -259,7 +318,7 @@
 
 /// Executes the given task. Stdout of the task is piped into the vsock stream to the
 /// virtualizationservice in the host side.
-fn exec_task(task: &Task, service: &Strong<dyn IVirtualMachineService>) -> Result<()> {
+fn exec_task(task: &Task, service: &Strong<dyn IVirtualMachineService>) -> Result<i32> {
     info!("executing main task {:?}...", task);
     let mut command = build_command(task)?;
 
@@ -273,19 +332,7 @@
     }
 
     let exit_status = command.spawn()?.wait()?;
-    if let Some(code) = exit_status.code() {
-        info!("notifying payload finished");
-        service.notifyPayloadFinished(code)?;
-
-        if code == 0 {
-            info!("task successfully finished");
-        } else {
-            error!("task exited with exit code: {}", code);
-        }
-    } else {
-        error!("task terminated: {}", exit_status);
-    }
-    Ok(())
+    exit_status.code().ok_or_else(|| anyhow!("Failed to get exit_code from the paylaod."))
 }
 
 fn build_command(task: &Task) -> Result<Command> {
diff --git a/tests/hostside/java/android/virt/test/MicrodroidTestCase.java b/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
index 3f80cdf..c2ef566 100644
--- a/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
+++ b/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
@@ -34,7 +34,7 @@
     private static final String APK_NAME = "MicrodroidTestApp.apk";
     private static final String PACKAGE_NAME = "com.android.microdroid.test";
 
-    private static final int MIN_MEM_ARM64 = 125;
+    private static final int MIN_MEM_ARM64 = 256;
     private static final int MIN_MEM_X86_64 = 400;
 
     private int minMemorySize() throws DeviceNotAvailableException {
diff --git a/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl b/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl
index 97f6ca3..1a16f2a 100644
--- a/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl
+++ b/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl
@@ -64,4 +64,9 @@
      * Error code indicating that the payload is verified, but has changed since the last boot.
      */
     const int ERROR_PAYLOAD_CHANGED = 2;
+
+    /**
+     * Error code indicating that the payload config is invalid.
+     */
+    const int ERROR_PAYLOAD_INVALID_CONFIG = 3;
 }
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index af420f6..e6210e2 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -644,6 +644,7 @@
 struct VirtualMachine {
     instance: Arc<VmInstance>,
     /// Keeps our service process running as long as this VM instance exists.
+    #[allow(dead_code)]
     lazy_service_guard: LazyServiceGuard,
 }
 
@@ -775,7 +776,7 @@
 
 /// The mutable state of the VirtualizationService. There should only be one instance of this
 /// struct.
-#[derive(Debug)]
+#[derive(Debug, Default)]
 struct State {
     /// The VMs which have been started. When VMs are started a weak reference is added to this list
     /// while a strong reference is returned to the caller over Binder. Once all copies of the
@@ -822,12 +823,6 @@
     }
 }
 
-impl Default for State {
-    fn default() -> Self {
-        State { vms: vec![], debug_held_vms: vec![] }
-    }
-}
-
 /// Get the next available CID, or an error if we have run out. The last CID used is stored in
 /// a system property so that restart of virtualizationservice doesn't reuse CID while the host
 /// Android is up.
diff --git a/zipfuse/Android.bp b/zipfuse/Android.bp
index 79e6bad..e10fc31 100644
--- a/zipfuse/Android.bp
+++ b/zipfuse/Android.bp
@@ -28,7 +28,6 @@
 rust_binary {
     name: "zipfuse",
     defaults: ["zipfuse.defaults"],
-    init_rc: ["zipfuse.rc"],
     bootstrap: true,
 }
 
diff --git a/zipfuse/zipfuse.rc b/zipfuse/zipfuse.rc
deleted file mode 100644
index 1905705..0000000
--- a/zipfuse/zipfuse.rc
+++ /dev/null
@@ -1,2 +0,0 @@
-service zipfuse /system/bin/zipfuse -o fscontext=u:object_r:zipfusefs:s0,context=u:object_r:system_file:s0 /dev/block/mapper/microdroid-apk /mnt/apk
-    disabled