Merge "Make exporting tombstone (out of VM) configurable"
diff --git a/apex/sign_virt_apex.py b/apex/sign_virt_apex.py
index e782bd2..a1e81d2 100644
--- a/apex/sign_virt_apex.py
+++ b/apex/sign_virt_apex.py
@@ -16,22 +16,63 @@
 """sign_virt_apex is a command line tool for sign the Virt APEX file.
 
 Typical usage:
-  sign_virt_apex [-v] [--avbtool path_to_avbtool] [--signing_args args] payload_key payload_dir
+  sign_virt_apex payload_key payload_dir
+    -v, --verbose
+    --verify
+    --avbtool path_to_avbtool
+    --signing_args args
 
 sign_virt_apex uses external tools which are assumed to be available via PATH.
 - avbtool (--avbtool can override the tool)
 - lpmake, lpunpack, simg2img, img2simg
 """
 import argparse
-import glob
 import hashlib
 import os
 import re
 import shlex
-import shutil
 import subprocess
 import sys
 import tempfile
+import traceback
+from concurrent import futures
+
+# pylint: disable=line-too-long,consider-using-with
+
+# Use executor to parallelize the invocation of external tools
+# If a task depends on another, pass the future object of the previous task as wait list.
+# Every future object created by a task should be consumed with AwaitAll()
+# so that exceptions are propagated .
+executor = futures.ThreadPoolExecutor()
+
+# Temporary directory for unpacked super.img.
+# We could put its creation/deletion into the task graph as well, but
+# having it as a global setup is much simpler.
+unpack_dir = tempfile.TemporaryDirectory()
+
+# tasks created with Async() are kept in a list so that they are awaited
+# before exit.
+tasks = []
+
+# create an async task and return a future value of it.
+def Async(fn, *args, wait=None, **kwargs):
+
+    # wrap a function with AwaitAll()
+    def wrapped():
+        AwaitAll(wait)
+        fn(*args, **kwargs)
+
+    task = executor.submit(wrapped)
+    tasks.append(task)
+    return task
+
+
+# waits for task (captured in fs as future values) with future.result()
+# so that any exception raised during task can be raised upward.
+def AwaitAll(fs):
+    if fs:
+        for f in fs:
+            f.result()
 
 
 def ParseArgs(argv):
@@ -71,7 +112,8 @@
     return args
 
 
-def RunCommand(args, cmd, env=None, expected_return_values={0}):
+def RunCommand(args, cmd, env=None, expected_return_values=None):
+    expected_return_values = expected_return_values or {0}
     env = env or {}
     env.update(os.environ.copy())
 
@@ -218,7 +260,7 @@
     if info is None:
         return
 
-    with TempDirectory() as work_dir:
+    with tempfile.TemporaryDirectory() as work_dir:
         algorithm = info['Algorithm']
         rollback_index = info['Rollback Index']
         rollback_index_location = info['Rollback Index Location']
@@ -254,18 +296,14 @@
             f.truncate(65536)
 
 
-class TempDirectory(object):
-
-    def __enter__(self):
-        self.name = tempfile.mkdtemp()
-        return self.name
-
-    def __exit__(self, *unused):
-        shutil.rmtree(self.name)
+def UnpackSuperImg(args, super_img, work_dir):
+    tmp_super_img = os.path.join(work_dir, 'super.img')
+    RunCommand(args, ['simg2img', super_img, tmp_super_img])
+    RunCommand(args, ['lpunpack', tmp_super_img, work_dir])
 
 
 def MakeSuperImage(args, partitions, output):
-    with TempDirectory() as work_dir:
+    with tempfile.TemporaryDirectory() as work_dir:
         cmd = ['lpmake', '--device-size=auto', '--metadata-slots=2',  # A/B
                '--metadata-size=65536', '--sparse', '--output=' + output]
 
@@ -281,6 +319,22 @@
         RunCommand(args, cmd)
 
 
+def SignSuperImg(args, key, super_img, work_dir):
+    # unpack super.img
+    UnpackSuperImg(args, super_img, work_dir)
+
+    system_a_img = os.path.join(work_dir, 'system_a.img')
+    vendor_a_img = os.path.join(work_dir, 'vendor_a.img')
+
+    # re-sign each partition
+    system_a_f = Async(AddHashTreeFooter, args, key, system_a_img)
+    vendor_a_f = Async(AddHashTreeFooter, args, key, vendor_a_img)
+
+    # 3. re-pack super.img
+    partitions = {"system_a": system_a_img, "vendor_a": vendor_a_img}
+    Async(MakeSuperImage, args, partitions, super_img, wait=[system_a_f, vendor_a_f])
+
+
 def ReplaceBootloaderPubkey(args, key, bootloader, bootloader_pubkey):
     if os.path.basename(bootloader) in args.key_overrides:
         key = args.key_overrides[os.path.basename(bootloader)]
@@ -305,134 +359,116 @@
         bl_f.write(new_pubkey)
 
 
+# dict of (key, file) for re-sign/verification. keys are un-versioned for readability.
+virt_apex_files = {
+    'bootloader.pubkey': 'etc/microdroid_bootloader.avbpubkey',
+    'bootloader': 'etc/microdroid_bootloader',
+    'boot.img': 'etc/fs/microdroid_boot-5.10.img',
+    'vendor_boot.img': 'etc/fs/microdroid_vendor_boot-5.10.img',
+    'init_boot.img': 'etc/fs/microdroid_init_boot.img',
+    'super.img': 'etc/fs/microdroid_super.img',
+    'vbmeta.img': 'etc/fs/microdroid_vbmeta.img',
+    'vbmeta_bootconfig.img': 'etc/fs/microdroid_vbmeta_bootconfig.img',
+    'bootconfig.normal': 'etc/microdroid_bootconfig.normal',
+    'bootconfig.app_debuggable': 'etc/microdroid_bootconfig.app_debuggable',
+    'bootconfig.full_debuggable': 'etc/microdroid_bootconfig.full_debuggable',
+    'uboot_env.img': 'etc/uboot_env.img'
+}
+
+
+def TargetFiles(input_dir):
+    return {k: os.path.join(input_dir, v) for k, v in virt_apex_files.items()}
+
+
 def SignVirtApex(args):
     key = args.key
     input_dir = args.input_dir
+    files = TargetFiles(input_dir)
 
-    # target files in the Virt APEX
-    bootloader_pubkey = os.path.join(
-        input_dir, 'etc', 'microdroid_bootloader.avbpubkey')
-    bootloader = os.path.join(input_dir, 'etc', 'microdroid_bootloader')
-    boot_img = os.path.join(input_dir, 'etc', 'fs', 'microdroid_boot-5.10.img')
-    vendor_boot_img = os.path.join(
-        input_dir, 'etc', 'fs', 'microdroid_vendor_boot-5.10.img')
-    init_boot_img = os.path.join(
-        input_dir, 'etc', 'fs', 'microdroid_init_boot.img')
-    super_img = os.path.join(input_dir, 'etc', 'fs', 'microdroid_super.img')
-    vbmeta_img = os.path.join(input_dir, 'etc', 'fs', 'microdroid_vbmeta.img')
-    vbmeta_bootconfig_img = os.path.join(
-        input_dir, 'etc', 'fs', 'microdroid_vbmeta_bootconfig.img')
-    bootconfig_normal = os.path.join(
-        input_dir, 'etc', 'microdroid_bootconfig.normal')
-    bootconfig_app_debuggable = os.path.join(
-        input_dir, 'etc', 'microdroid_bootconfig.app_debuggable')
-    bootconfig_full_debuggable = os.path.join(
-        input_dir, 'etc', 'microdroid_bootconfig.full_debuggable')
-    uboot_env_img = os.path.join(
-        input_dir, 'etc', 'uboot_env.img')
+    # unpacked files (will be unpacked from super.img below)
+    system_a_img = os.path.join(unpack_dir.name, 'system_a.img')
+    vendor_a_img = os.path.join(unpack_dir.name, 'vendor_a.img')
 
-    # Key(pubkey) for bootloader should match with the one used to make VBmeta below
+    # Key(pubkey) embedded in bootloader should match with the one used to make VBmeta below
     # while it's okay to use different keys for other image files.
-    ReplaceBootloaderPubkey(args, key, bootloader, bootloader_pubkey)
+    replace_f = Async(ReplaceBootloaderPubkey, args,
+                      key, files['bootloader'], files['bootloader.pubkey'])
 
     # re-sign bootloader, boot.img, vendor_boot.img, and init_boot.img
-    AddHashFooter(args, key, bootloader)
-    AddHashFooter(args, key, boot_img)
-    AddHashFooter(args, key, vendor_boot_img)
-    AddHashFooter(args, key, init_boot_img)
+    Async(AddHashFooter, args, key, files['bootloader'], wait=[replace_f])
+    boot_img_f = Async(AddHashFooter, args, key, files['boot.img'])
+    vendor_boot_img_f = Async(AddHashFooter, args, key, files['vendor_boot.img'])
+    init_boot_img_f = Async(AddHashFooter, args, key, files['init_boot.img'])
 
     # re-sign super.img
-    with TempDirectory() as work_dir:
-        # unpack super.img
-        tmp_super_img = os.path.join(work_dir, 'super.img')
-        RunCommand(args, ['simg2img', super_img, tmp_super_img])
-        RunCommand(args, ['lpunpack', tmp_super_img, work_dir])
+    super_img_f = Async(SignSuperImg, args, key, files['super.img'], unpack_dir.name)
 
-        system_a_img = os.path.join(work_dir, 'system_a.img')
-        vendor_a_img = os.path.join(work_dir, 'vendor_a.img')
-        partitions = {"system_a": system_a_img, "vendor_a": vendor_a_img}
-
-        # re-sign partitions in super.img
-        for img in partitions.values():
-            AddHashTreeFooter(args, key, img)
-
-        # re-pack super.img
-        MakeSuperImage(args, partitions, super_img)
-
-        # re-generate vbmeta from re-signed {boot, vendor_boot, init_boot, system_a, vendor_a}.img
-        # Ideally, making VBmeta should be done out of TempDirectory block. But doing it here
-        # to avoid unpacking re-signed super.img for system/vendor images which are available
-        # in this block.
-        MakeVbmetaImage(args, key, vbmeta_img, images=[
-                        boot_img, vendor_boot_img, init_boot_img, system_a_img, vendor_a_img])
+    # re-generate vbmeta from re-signed {boot, vendor_boot, init_boot, system_a, vendor_a}.img
+    Async(MakeVbmetaImage, args, key, files['vbmeta.img'],
+          images=[files['boot.img'], files['vendor_boot.img'],
+                  files['init_boot.img'], system_a_img, vendor_a_img],
+          wait=[boot_img_f, vendor_boot_img_f, init_boot_img_f, super_img_f])
 
     # Re-sign bootconfigs and the uboot_env with the same key
     bootconfig_sign_key = key
-    AddHashFooter(args, bootconfig_sign_key, bootconfig_normal)
-    AddHashFooter(args, bootconfig_sign_key, bootconfig_app_debuggable)
-    AddHashFooter(args, bootconfig_sign_key, bootconfig_full_debuggable)
-    AddHashFooter(args, bootconfig_sign_key, uboot_env_img)
+    Async(AddHashFooter, args, bootconfig_sign_key, files['bootconfig.normal'])
+    Async(AddHashFooter, args, bootconfig_sign_key, files['bootconfig.app_debuggable'])
+    Async(AddHashFooter, args, bootconfig_sign_key, files['bootconfig.full_debuggable'])
+    Async(AddHashFooter, args, bootconfig_sign_key, files['uboot_env.img'])
 
     # Re-sign vbmeta_bootconfig with chained_partitions to "bootconfig" and
     # "uboot_env". Note that, for now, `key` and `bootconfig_sign_key` are the
     # same, but technically they can be different. Vbmeta records pubkeys which
     # signed chained partitions.
-    MakeVbmetaImage(args, key, vbmeta_bootconfig_img, chained_partitions={
-                    'bootconfig': bootconfig_sign_key,
-                    'uboot_env': bootconfig_sign_key,
+    Async(MakeVbmetaImage, args, key, files['vbmeta_bootconfig.img'], chained_partitions={
+        'bootconfig': bootconfig_sign_key,
+        'uboot_env': bootconfig_sign_key,
     })
 
 
 def VerifyVirtApex(args):
-    # Generator to emit avbtool-signed items along with its pubkey digest.
-    # This supports lpmake-packed images as well.
-    def Recur(target_dir):
-        for file in glob.glob(os.path.join(target_dir, 'etc', '**', '*'), recursive=True):
-            cur_item = os.path.relpath(file, target_dir)
+    key = args.key
+    input_dir = args.input_dir
+    files = TargetFiles(input_dir)
 
-            if not os.path.isfile(file):
-                continue
+    # unpacked files
+    UnpackSuperImg(args, files['super.img'], unpack_dir.name)
+    system_a_img = os.path.join(unpack_dir.name, 'system_a.img')
+    vendor_a_img = os.path.join(unpack_dir.name, 'vendor_a.img')
 
-            # avbpubkey
-            if cur_item == 'etc/microdroid_bootloader.avbpubkey':
-                with open(file, 'rb') as f:
-                    yield (cur_item, hashlib.sha1(f.read()).hexdigest())
-                continue
+    # Read pubkey digest from the input key
+    with tempfile.NamedTemporaryFile() as pubkey_file:
+        ExtractAvbPubkey(args, key, pubkey_file.name)
+        with open(pubkey_file.name, 'rb') as f:
+            pubkey = f.read()
+            pubkey_digest = hashlib.sha1(pubkey).hexdigest()
 
-            # avbtool signed
-            info, _ = AvbInfo(args, file)
-            if info:
-                yield (cur_item, info['Public key (sha1)'])
-                continue
+    def contents(file):
+        with open(file, 'rb') as f:
+            return f.read()
 
-            # logical partition
-            with TempDirectory() as tmp_dir:
-                unsparsed = os.path.join(tmp_dir, os.path.basename(file))
-                _, rc = RunCommand(
-                    # exit with 255 if it's not sparsed
-                    args, ['simg2img', file, unsparsed], expected_return_values={0, 255})
-                if rc == 0:
-                    with TempDirectory() as unpack_dir:
-                        # exit with 64 if it's not a logical partition.
-                        _, rc = RunCommand(
-                            args, ['lpunpack', unsparsed, unpack_dir], expected_return_values={0, 64})
-                        if rc == 0:
-                            nested_items = list(Recur(unpack_dir))
-                            if len(nested_items) > 0:
-                                for (item, key) in nested_items:
-                                    yield ('%s!/%s' % (cur_item, item), key)
-                                continue
-    # Read pubkey digest
-    with TempDirectory() as tmp_dir:
-        pubkey_file = os.path.join(tmp_dir, 'avbpubkey')
-        ExtractAvbPubkey(args, args.key, pubkey_file)
-        with open(pubkey_file, 'rb') as f:
-            pubkey_digest = hashlib.sha1(f.read()).hexdigest()
+    def check_equals_pubkey(file):
+        assert contents(file) == pubkey, 'pubkey mismatch: %s' % file
 
-    # Check every avbtool-signed item against the input key
-    for (item, pubkey) in Recur(args.input_dir):
-        assert pubkey == pubkey_digest, '%s: key mismatch: %s != %s' % (
-            item, pubkey, pubkey_digest)
+    def check_contains_pubkey(file):
+        assert contents(file).find(pubkey) != -1, 'pubkey missing: %s' % file
+
+    def check_avb_pubkey(file):
+        info, _ = AvbInfo(args, file)
+        assert info is not None, 'no avbinfo: %s' % file
+        assert info['Public key (sha1)'] == pubkey_digest, 'pubkey mismatch: %s' % file
+
+    for f in files.values():
+        if f == files['bootloader.pubkey']:
+            Async(check_equals_pubkey, f)
+        elif f == files['bootloader']:
+            Async(check_contains_pubkey, f)
+        elif f == files['super.img']:
+            Async(check_avb_pubkey, system_a_img)
+            Async(check_avb_pubkey, vendor_a_img)
+        else:
+            # Check pubkey for other files using avbtool
+            Async(check_avb_pubkey, f)
 
 
 def main(argv):
@@ -442,8 +478,10 @@
             VerifyVirtApex(args)
         else:
             SignVirtApex(args)
-    except Exception as e:
-        print(e)
+        # ensure all tasks are completed without exceptions
+        AwaitAll(tasks)
+    except: # pylint: disable=bare-except
+        traceback.print_exc()
         sys.exit(1)
 
 
diff --git a/apex/virtualizationservice.rc b/apex/virtualizationservice.rc
index 7e71105..02b2081 100644
--- a/apex/virtualizationservice.rc
+++ b/apex/virtualizationservice.rc
@@ -14,8 +14,8 @@
 
 service virtualizationservice /apex/com.android.virt/bin/virtualizationservice
     class main
-    user virtualizationservice
-    group virtualizationservice
+    user system
+    group system
     interface aidl android.system.virtualizationservice
     disabled
     oneshot
diff --git a/apkdmverity/src/loopdevice.rs b/apkdmverity/src/loopdevice.rs
index 376abd4..35ae154 100644
--- a/apkdmverity/src/loopdevice.rs
+++ b/apkdmverity/src/loopdevice.rs
@@ -25,8 +25,10 @@
 
 use anyhow::{Context, Result};
 use data_model::DataInit;
+use libc::O_DIRECT;
 use std::fs::{File, OpenOptions};
 use std::mem::size_of;
+use std::os::unix::fs::OpenOptionsExt;
 use std::os::unix::io::AsRawFd;
 use std::path::{Path, PathBuf};
 use std::thread;
@@ -37,8 +39,7 @@
 
 // These are old-style ioctls, thus *_bad.
 nix::ioctl_none_bad!(_loop_ctl_get_free, LOOP_CTL_GET_FREE);
-nix::ioctl_write_int_bad!(_loop_set_fd, LOOP_SET_FD);
-nix::ioctl_write_ptr_bad!(_loop_set_status64, LOOP_SET_STATUS64, loop_info64);
+nix::ioctl_write_ptr_bad!(_loop_configure, LOOP_CONFIGURE, loop_config);
 #[cfg(test)]
 nix::ioctl_none_bad!(_loop_clr_fd, LOOP_CLR_FD);
 
@@ -49,14 +50,9 @@
     Ok(unsafe { _loop_ctl_get_free(ctrl_file.as_raw_fd()) }?)
 }
 
-fn loop_set_fd(device_file: &File, fd: i32) -> Result<i32> {
+fn loop_configure(device_file: &File, config: &loop_config) -> Result<i32> {
     // SAFETY: this ioctl changes the state in kernel, but not the state in this process.
-    Ok(unsafe { _loop_set_fd(device_file.as_raw_fd(), fd) }?)
-}
-
-fn loop_set_status64(device_file: &File, info: &loop_info64) -> Result<i32> {
-    // SAFETY: this ioctl changes the state in kernel, but not the state in this process.
-    Ok(unsafe { _loop_set_status64(device_file.as_raw_fd(), info) }?)
+    Ok(unsafe { _loop_configure(device_file.as_raw_fd(), config) }?)
 }
 
 #[cfg(test)]
@@ -67,7 +63,12 @@
 }
 
 /// Creates a loop device and attach the given file at `path` as the backing store.
-pub fn attach<P: AsRef<Path>>(path: P, offset: u64, size_limit: u64) -> Result<PathBuf> {
+pub fn attach<P: AsRef<Path>>(
+    path: P,
+    offset: u64,
+    size_limit: u64,
+    direct_io: bool,
+) -> Result<PathBuf> {
     // Attaching a file to a loop device can make a race condition; a loop device number obtained
     // from LOOP_CTL_GET_FREE might have been used by another thread or process. In that case the
     // subsequet LOOP_CONFIGURE ioctl returns with EBUSY. Try until it succeeds.
@@ -81,7 +82,7 @@
 
     let begin = Instant::now();
     loop {
-        match try_attach(&path, offset, size_limit) {
+        match try_attach(&path, offset, size_limit, direct_io) {
             Ok(loop_dev) => return Ok(loop_dev),
             Err(e) => {
                 if begin.elapsed() > TIMEOUT {
@@ -99,7 +100,12 @@
 #[cfg(target_os = "android")]
 const LOOP_DEV_PREFIX: &str = "/dev/block/loop";
 
-fn try_attach<P: AsRef<Path>>(path: P, offset: u64, size_limit: u64) -> Result<PathBuf> {
+fn try_attach<P: AsRef<Path>>(
+    path: P,
+    offset: u64,
+    size_limit: u64,
+    direct_io: bool,
+) -> Result<PathBuf> {
     // Get a free loop device
     wait_for_path(LOOP_CONTROL)?;
     let ctrl_file = OpenOptions::new()
@@ -112,21 +118,19 @@
     // Construct the loop_info64 struct
     let backing_file = OpenOptions::new()
         .read(true)
+        .custom_flags(if direct_io { O_DIRECT } else { 0 })
         .open(&path)
         .context(format!("failed to open {:?}", path.as_ref()))?;
     // safe because the size of the array is the same as the size of the struct
-    let mut info: loop_info64 =
-        *DataInit::from_mut_slice(&mut [0; size_of::<loop_info64>()]).unwrap();
-    info.lo_offset = offset;
-    info.lo_sizelimit = size_limit;
-    info.lo_flags |= Flag::LO_FLAGS_DIRECT_IO | Flag::LO_FLAGS_READ_ONLY;
-
-    // Special case: don't use direct IO when the backing file is already a loop device, which
-    // happens only during test. DirectIO-on-loop-over-loop makes the outer loop device
-    // unaccessible.
-    #[cfg(test)]
-    if path.as_ref().to_str().unwrap().starts_with(LOOP_DEV_PREFIX) {
-        info.lo_flags.remove(Flag::LO_FLAGS_DIRECT_IO);
+    let mut config: loop_config =
+        *DataInit::from_mut_slice(&mut [0; size_of::<loop_config>()]).unwrap();
+    config.fd = backing_file.as_raw_fd() as u32;
+    config.block_size = 4096;
+    config.info.lo_offset = offset;
+    config.info.lo_sizelimit = size_limit;
+    config.info.lo_flags = Flag::LO_FLAGS_READ_ONLY;
+    if direct_io {
+        config.info.lo_flags.insert(Flag::LO_FLAGS_DIRECT_IO);
     }
 
     // Configure the loop device to attach the backing file
@@ -137,8 +141,8 @@
         .write(true)
         .open(&device_path)
         .context(format!("failed to open {:?}", &device_path))?;
-    loop_set_fd(&device_file, backing_file.as_raw_fd() as i32)?;
-    loop_set_status64(&device_file, &info)?;
+    loop_configure(&device_file, &config)
+        .context(format!("Failed to configure {:?}", &device_path))?;
 
     Ok(PathBuf::from(device_path))
 }
@@ -150,3 +154,46 @@
     loop_clr_fd(&device_file)?;
     Ok(())
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::fs;
+    use std::path::Path;
+
+    fn create_empty_file(path: &Path, size: u64) {
+        let f = File::create(path).unwrap();
+        f.set_len(size).unwrap();
+    }
+
+    fn is_direct_io(dev: &Path) -> bool {
+        let dio = Path::new("/sys/block").join(dev.file_name().unwrap()).join("loop/dio");
+        "1" == fs::read_to_string(&dio).unwrap().trim()
+    }
+
+    #[test]
+    fn attach_loop_device_with_dio() {
+        let a_dir = tempfile::TempDir::new().unwrap();
+        let a_file = a_dir.path().join("test");
+        let a_size = 4096u64;
+        create_empty_file(&a_file, a_size);
+        let dev = attach(a_file, 0, a_size, /*direct_io*/ true).unwrap();
+        scopeguard::defer! {
+            detach(&dev).unwrap();
+        }
+        assert!(is_direct_io(&dev));
+    }
+
+    #[test]
+    fn attach_loop_device_without_dio() {
+        let a_dir = tempfile::TempDir::new().unwrap();
+        let a_file = a_dir.path().join("test");
+        let a_size = 4096u64;
+        create_empty_file(&a_file, a_size);
+        let dev = attach(a_file, 0, a_size, /*direct_io*/ false).unwrap();
+        scopeguard::defer! {
+            detach(&dev).unwrap();
+        }
+        assert!(!is_direct_io(&dev));
+    }
+}
diff --git a/apkdmverity/src/loopdevice/sys.rs b/apkdmverity/src/loopdevice/sys.rs
index d32987a..fa87548 100644
--- a/apkdmverity/src/loopdevice/sys.rs
+++ b/apkdmverity/src/loopdevice/sys.rs
@@ -24,8 +24,7 @@
 pub const LOOP_CONTROL: &str = "/dev/loop-control";
 
 pub const LOOP_CTL_GET_FREE: libc::c_ulong = 0x4C82;
-pub const LOOP_SET_FD: libc::c_ulong = 0x4C00;
-pub const LOOP_SET_STATUS64: libc::c_ulong = 0x4C04;
+pub const LOOP_CONFIGURE: libc::c_ulong = 0x4C0A;
 #[cfg(test)]
 pub const LOOP_CLR_FD: libc::c_ulong = 0x4C01;
 
diff --git a/apkdmverity/src/main.rs b/apkdmverity/src/main.rs
index dbf3131..16dd480 100644
--- a/apkdmverity/src/main.rs
+++ b/apkdmverity/src/main.rs
@@ -94,7 +94,11 @@
         if apk_size % BLOCK_SIZE != 0 {
             bail!("The size of {:?} is not multiple of {}.", &apk, BLOCK_SIZE)
         }
-        (loopdevice::attach(&apk, 0, apk_size)?, apk_size)
+        (
+            loopdevice::attach(&apk, 0, apk_size, /*direct_io*/ true)
+                .context("Failed to attach APK to a loop device")?,
+            apk_size,
+        )
     };
 
     // Parse the idsig file to locate the merkle tree in it, then attach the file to a loop device
@@ -105,7 +109,11 @@
     )?;
     let offset = sig.merkle_tree_offset;
     let size = sig.merkle_tree_size as u64;
-    let hash_device = loopdevice::attach(&idsig, offset, size)?;
+    // Due to unknown reason(b/191344832), we can't enable "direct IO" for the IDSIG file (backing
+    // the hash). For now we don't use "direct IO" but it seems OK since the IDSIG file is very
+    // small and the benefit of direct-IO would be negliable.
+    let hash_device = loopdevice::attach(&idsig, offset, size, /*direct_io*/ false)
+        .context("Failed to attach idsig to a loop device")?;
 
     // Build a dm-verity target spec from the information from the idsig file. The apk and the
     // idsig files are used as the data device and the hash device, respectively.
@@ -318,11 +326,11 @@
         // already a block device, `enable_verity` uses the block device as it is. The detatching
         // of the data device is done in the scopeguard for the return value of `enable_verity`
         // below. Only the idsig_loop_device needs detatching.
-        let apk_loop_device = loopdevice::attach(&apk_path, 0, apk_size).unwrap();
-        let idsig_loop_device =
-            scopeguard::guard(loopdevice::attach(&idsig_path, 0, idsig_size).unwrap(), |dev| {
-                loopdevice::detach(dev).unwrap()
-            });
+        let apk_loop_device = loopdevice::attach(&apk_path, 0, apk_size, true).unwrap();
+        let idsig_loop_device = scopeguard::guard(
+            loopdevice::attach(&idsig_path, 0, idsig_size, false).unwrap(),
+            |dev| loopdevice::detach(dev).unwrap(),
+        );
 
         let name = "loop_as_input";
         // Run the program WITH the loop devices, not the regular files.
diff --git a/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java b/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
index 5d36f16..64658a9 100644
--- a/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
+++ b/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
@@ -16,21 +16,28 @@
 
 package com.android.virt.fs;
 
+import static com.android.tradefed.device.TestDevice.MicrodroidBuilder;
 import static com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeFalse;
 
 import android.platform.test.annotations.RootPermissionTest;
 import android.virt.test.CommandRunner;
 import android.virt.test.VirtualizationTestCaseBase;
 
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
 import com.android.compatibility.common.util.PollingCheck;
+import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.TestDevice;
 import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
@@ -47,7 +54,8 @@
 import org.junit.rules.TestName;
 import org.junit.runner.RunWith;
 
-import java.util.Optional;
+import java.io.File;
+import java.io.FileNotFoundException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -62,6 +70,12 @@
     /** Output directory where the test can generate output on Android */
     private static final String TEST_OUTPUT_DIR = "/data/local/tmp/authfs/output_dir";
 
+    /** File name of the test APK */
+    private static final String TEST_APK_NAME = "MicrodroidTestApp.apk";
+
+    /** VM config entry path in the test APK */
+    private static final String VM_CONFIG_PATH_IN_APK = "assets/vm_config_extra_apk.json";
+
     /** Path to open_then_run on Android */
     private static final String OPEN_THEN_RUN_BIN = "/data/local/tmp/open_then_run";
 
@@ -75,9 +89,7 @@
     private static final String AUTHFS_BIN = "/system/bin/authfs";
 
     /** Idsig paths to be created for each APK in the "extra_apks" of vm_config_extra_apk.json. */
-    private static final String[] EXTRA_IDSIG_PATHS = new String[] {
-        TEST_DIR + "BuildManifest.apk.idsig",
-    };
+    private static final String EXTRA_IDSIG_PATH = TEST_DIR + "BuildManifest.apk.idsig";
 
     /** Build manifest path in the VM. 0 is the index of extra_apks in vm_config_extra_apk.json. */
     private static final String BUILD_MANIFEST_PATH = "/mnt/extra-apk/0/assets/build_manifest.pb";
@@ -99,7 +111,7 @@
     private static final int VMADDR_CID_HOST = 2;
 
     private static CommandRunner sAndroid;
-    private static String sCid;
+    private static CommandRunner sMicrodroid;
     private static boolean sAssumptionFailed;
 
     private ExecutorService mThreadPool = Executors.newCachedThreadPool();
@@ -127,30 +139,20 @@
             return;
         }
 
-        prepareVirtualizationTestSetup(androidDevice);
-
         // For each test case, boot and adb connect to a new Microdroid
         CLog.i("Starting the shared VM");
-        final String apkName = "MicrodroidTestApp.apk";
-        final String packageName = "com.android.microdroid.test";
-        final String configPath = "assets/vm_config_extra_apk.json"; // path inside the APK
-        sCid =
-                startMicrodroid(
-                        androidDevice,
-                        testInfo.getBuildInfo(),
-                        apkName,
-                        packageName,
-                        EXTRA_IDSIG_PATHS,
-                        configPath,
-                        /* debug */ true,
-                        /* use default memoryMib */ 0,
-                        Optional.empty(),
-                        Optional.empty());
-        adbConnectToMicrodroid(androidDevice, sCid);
+        ITestDevice microdroidDevice =
+                MicrodroidBuilder
+                        .fromFile(findTestApk(testInfo.getBuildInfo()), VM_CONFIG_PATH_IN_APK)
+                        .debugLevel("full")
+                        .addExtraIdsigPath(EXTRA_IDSIG_PATH)
+                        .build((TestDevice) androidDevice);
 
         // Root because authfs (started from shell in this test) currently require root to open
         // /dev/fuse and mount the FUSE.
-        rootMicrodroid();
+        assertThat(microdroidDevice.enableAdbRoot()).isTrue();
+
+        sMicrodroid = new CommandRunner(microdroidDevice);
     }
 
     @AfterClassWithInfo
@@ -158,13 +160,12 @@
             throws DeviceNotAvailableException {
         assertNotNull(sAndroid);
 
-        if (sCid != null) {
+        if (sMicrodroid != null) {
             CLog.i("Shutting down shared VM");
-            shutdownMicrodroid(sAndroid.getDevice(), sCid);
-            sCid = null;
+            ((TestDevice) testInfo.getDevice()).shutdownMicrodroid(sMicrodroid.getDevice());
+            sMicrodroid = null;
         }
 
-        cleanUpVirtualizationTestSetup(sAndroid.getDevice());
         sAndroid = null;
     }
 
@@ -176,9 +177,13 @@
 
     @After
     public void tearDown() throws Exception {
+        if (sMicrodroid != null) {
+            sMicrodroid.tryRun("killall authfs");
+            sMicrodroid.tryRun("umount " + MOUNT_DIR);
+        }
+
+        assertNotNull(sAndroid);
         sAndroid.tryRun("killall fd_server");
-        tryRunOnMicrodroid("killall authfs");
-        tryRunOnMicrodroid("umount " + MOUNT_DIR);
 
         // Even though we only run one VM for the whole class, and could have collect the VM log
         // after all tests are done, TestLogData doesn't seem to work at class level. Hence,
@@ -319,7 +324,7 @@
 
         // Verify
         // Force dropping the page cache, so that the next read can be validated.
-        runOnMicrodroid("echo 1 > /proc/sys/vm/drop_caches");
+        sMicrodroid.run("echo 1 > /proc/sys/vm/drop_caches");
         // A read will fail if the backing data has been tampered.
         assertFalse(checkReadAtFileOffsetOnMicrodroid(
                 destPath, /* offset */ 0, /* number */ 4096));
@@ -419,8 +424,8 @@
 
         // Action
         // Can create nested directories and can create a file in one.
-        runOnMicrodroid("mkdir " + authfsOutputDir + "/new_dir");
-        runOnMicrodroid("mkdir -p " + authfsOutputDir + "/we/need/to/go/deeper");
+        sMicrodroid.run("mkdir " + authfsOutputDir + "/new_dir");
+        sMicrodroid.run("mkdir -p " + authfsOutputDir + "/we/need/to/go/deeper");
         createFileWithOnesOnMicrodroid(authfsOutputDir + "/new_dir/file1", 10000);
         createFileWithOnesOnMicrodroid(authfsOutputDir + "/we/need/file2", 10000);
 
@@ -452,7 +457,7 @@
         runAuthFsOnMicrodroid("--remote-new-rw-dir 3 --cid " + VMADDR_CID_HOST);
 
         // Action & Verify
-        runOnMicrodroid("echo -n foo > " + authfsOutputDir + "/file");
+        sMicrodroid.run("echo -n foo > " + authfsOutputDir + "/file");
         assertEquals(getFileSizeInBytesOnMicrodroid(authfsOutputDir + "/file"), 3);
         // Can override a file and write normally.
         createFileWithOnesOnMicrodroid(authfsOutputDir + "/file", 10000);
@@ -472,13 +477,13 @@
         runFdServerOnAndroid("--open-dir 3:" + androidOutputDir, "--rw-dirs 3");
         runAuthFsOnMicrodroid("--remote-new-rw-dir 3 --cid " + VMADDR_CID_HOST);
 
-        runOnMicrodroid("echo -n foo > " + authfsOutputDir + "/file");
-        runOnMicrodroid("test -f " + authfsOutputDir + "/file");
+        sMicrodroid.run("echo -n foo > " + authfsOutputDir + "/file");
+        sMicrodroid.run("test -f " + authfsOutputDir + "/file");
         sAndroid.run("test -f " + androidOutputDir + "/file");
 
         // Action & Verify
-        runOnMicrodroid("rm " + authfsOutputDir + "/file");
-        runOnMicrodroid("test ! -f " + authfsOutputDir + "/file");
+        sMicrodroid.run("rm " + authfsOutputDir + "/file");
+        sMicrodroid.run("test ! -f " + authfsOutputDir + "/file");
         sAndroid.run("test ! -f " + androidOutputDir + "/file");
     }
 
@@ -491,20 +496,20 @@
         runFdServerOnAndroid("--open-dir 3:" + androidOutputDir, "--rw-dirs 3");
         runAuthFsOnMicrodroid("--remote-new-rw-dir 3 --cid " + VMADDR_CID_HOST);
 
-        runOnMicrodroid("mkdir -p " + authfsOutputDir + "/dir/dir2");
-        runOnMicrodroid("echo -n foo > " + authfsOutputDir + "/dir/file");
+        sMicrodroid.run("mkdir -p " + authfsOutputDir + "/dir/dir2");
+        sMicrodroid.run("echo -n foo > " + authfsOutputDir + "/dir/file");
         sAndroid.run("test -d " + androidOutputDir + "/dir/dir2");
 
         // Action & Verify
-        runOnMicrodroid("rmdir " + authfsOutputDir + "/dir/dir2");
-        runOnMicrodroid("test ! -d " + authfsOutputDir + "/dir/dir2");
+        sMicrodroid.run("rmdir " + authfsOutputDir + "/dir/dir2");
+        sMicrodroid.run("test ! -d " + authfsOutputDir + "/dir/dir2");
         sAndroid.run("test ! -d " + androidOutputDir + "/dir/dir2");
         // Can only delete a directory if empty
         assertFailedOnMicrodroid("rmdir " + authfsOutputDir + "/dir");
-        runOnMicrodroid("test -d " + authfsOutputDir + "/dir");  // still there
-        runOnMicrodroid("rm " + authfsOutputDir + "/dir/file");
-        runOnMicrodroid("rmdir " + authfsOutputDir + "/dir");
-        runOnMicrodroid("test ! -d " + authfsOutputDir + "/dir");
+        sMicrodroid.run("test -d " + authfsOutputDir + "/dir");  // still there
+        sMicrodroid.run("rm " + authfsOutputDir + "/dir/file");
+        sMicrodroid.run("rmdir " + authfsOutputDir + "/dir");
+        sMicrodroid.run("test ! -d " + authfsOutputDir + "/dir");
         sAndroid.run("test ! -d " + androidOutputDir + "/dir");
     }
 
@@ -517,10 +522,10 @@
         runFdServerOnAndroid("--open-dir 3:" + androidOutputDir, "--rw-dirs 3");
         runAuthFsOnMicrodroid("--remote-new-rw-dir 3 --cid " + VMADDR_CID_HOST);
 
-        runOnMicrodroid("touch " + authfsOutputDir + "/some_file");
-        runOnMicrodroid("mkdir " + authfsOutputDir + "/some_dir");
-        runOnMicrodroid("touch " + authfsOutputDir + "/some_dir/file");
-        runOnMicrodroid("mkdir " + authfsOutputDir + "/some_dir/dir");
+        sMicrodroid.run("touch " + authfsOutputDir + "/some_file");
+        sMicrodroid.run("mkdir " + authfsOutputDir + "/some_dir");
+        sMicrodroid.run("touch " + authfsOutputDir + "/some_dir/file");
+        sMicrodroid.run("mkdir " + authfsOutputDir + "/some_dir/dir");
 
         // Action & Verify
         // Cannot create directory if an entry with the same name already exists.
@@ -542,12 +547,12 @@
         // Create a file with some data. Test the existence.
         String outputPath = authfsOutputDir + "/out";
         String androidOutputPath = androidOutputDir + "/out";
-        runOnMicrodroid("echo -n 123 > " + outputPath);
-        runOnMicrodroid("test -f " + outputPath);
+        sMicrodroid.run("echo -n 123 > " + outputPath);
+        sMicrodroid.run("test -f " + outputPath);
         sAndroid.run("test -f " + androidOutputPath);
 
         // Action
-        String output = runOnMicrodroid(
+        String output = sMicrodroid.run(
                 // Open the file for append and read
                 "exec 4>>" + outputPath + " 5<" + outputPath + "; "
                 // Delete the file from the directory
@@ -560,7 +565,7 @@
         // Verify
         // Output contains all written data, while the files are deleted.
         assertEquals("123456", output);
-        runOnMicrodroid("test ! -f " + outputPath);
+        sMicrodroid.run("test ! -f " + outputPath);
         sAndroid.run("test ! -f " + androidOutputDir + "/out");
     }
 
@@ -590,7 +595,7 @@
                 + VMADDR_CID_HOST);
 
         // Verify
-        runOnMicrodroid("test -f " + authfsInputDir + "/system/framework/services.jar");
+        sMicrodroid.run("test -f " + authfsInputDir + "/system/framework/services.jar");
         assertFailedOnMicrodroid("test -f " + authfsInputDir + "/system/bin/sh");
     }
 
@@ -602,14 +607,14 @@
 
         // Action
         String authfsOutputDir = MOUNT_DIR + "/3";
-        runOnMicrodroid("mkdir -p " + authfsOutputDir + "/dir/dir2/dir3");
-        runOnMicrodroid("touch " + authfsOutputDir + "/dir/dir2/dir3/file1");
-        runOnMicrodroid("touch " + authfsOutputDir + "/dir/dir2/dir3/file2");
-        runOnMicrodroid("touch " + authfsOutputDir + "/dir/dir2/dir3/file3");
-        runOnMicrodroid("touch " + authfsOutputDir + "/file");
+        sMicrodroid.run("mkdir -p " + authfsOutputDir + "/dir/dir2/dir3");
+        sMicrodroid.run("touch " + authfsOutputDir + "/dir/dir2/dir3/file1");
+        sMicrodroid.run("touch " + authfsOutputDir + "/dir/dir2/dir3/file2");
+        sMicrodroid.run("touch " + authfsOutputDir + "/dir/dir2/dir3/file3");
+        sMicrodroid.run("touch " + authfsOutputDir + "/file");
 
         // Verify
-        String[] actual = runOnMicrodroid("cd " + authfsOutputDir + "; find |sort").split("\n");
+        String[] actual = sMicrodroid.run("cd " + authfsOutputDir + "; find |sort").split("\n");
         String[] expected = new String[] {
                 ".",
                 "./dir",
@@ -622,14 +627,14 @@
         assertEquals(expected, actual);
 
         // Add more entries.
-        runOnMicrodroid("mkdir -p " + authfsOutputDir + "/dir2");
-        runOnMicrodroid("touch " + authfsOutputDir + "/file2");
+        sMicrodroid.run("mkdir -p " + authfsOutputDir + "/dir2");
+        sMicrodroid.run("touch " + authfsOutputDir + "/file2");
         // Check new entries. Also check that the types are correct.
-        actual = runOnMicrodroid(
+        actual = sMicrodroid.run(
                 "cd " + authfsOutputDir + "; find -maxdepth 1 -type f |sort").split("\n");
         expected = new String[] {"./file", "./file2"};
         assertEquals(expected, actual);
-        actual = runOnMicrodroid(
+        actual = sMicrodroid.run(
                 "cd " + authfsOutputDir + "; find -maxdepth 1 -type d |sort").split("\n");
         expected = new String[] {".", "./dir", "./dir2"};
         assertEquals(expected, actual);
@@ -643,7 +648,7 @@
 
         // Action & Verify
         // Change mode
-        runOnMicrodroid("chmod 321 " + MOUNT_DIR + "/3");
+        sMicrodroid.run("chmod 321 " + MOUNT_DIR + "/3");
         expectFileMode("--wx-w---x", MOUNT_DIR + "/3", TEST_OUTPUT_DIR + "/file");
         // Can't set the disallowed bits
         assertFailedOnMicrodroid("chmod +s " + MOUNT_DIR + "/3");
@@ -659,14 +664,14 @@
         // Action & Verify
         String authfsOutputDir = MOUNT_DIR + "/3";
         // Create with umask
-        runOnMicrodroid("umask 000; mkdir " + authfsOutputDir + "/dir");
-        runOnMicrodroid("umask 022; mkdir " + authfsOutputDir + "/dir/dir2");
+        sMicrodroid.run("umask 000; mkdir " + authfsOutputDir + "/dir");
+        sMicrodroid.run("umask 022; mkdir " + authfsOutputDir + "/dir/dir2");
         expectFileMode("drwxrwxrwx", authfsOutputDir + "/dir", TEST_OUTPUT_DIR + "/dir");
         expectFileMode("drwxr-xr-x", authfsOutputDir + "/dir/dir2", TEST_OUTPUT_DIR + "/dir/dir2");
         // Change mode
-        runOnMicrodroid("chmod -w " + authfsOutputDir + "/dir/dir2");
+        sMicrodroid.run("chmod -w " + authfsOutputDir + "/dir/dir2");
         expectFileMode("dr-xr-xr-x", authfsOutputDir + "/dir/dir2", TEST_OUTPUT_DIR + "/dir/dir2");
-        runOnMicrodroid("chmod 321 " + authfsOutputDir + "/dir");
+        sMicrodroid.run("chmod 321 " + authfsOutputDir + "/dir");
         expectFileMode("d-wx-w---x", authfsOutputDir + "/dir", TEST_OUTPUT_DIR + "/dir");
         // Can't set the disallowed bits
         assertFailedOnMicrodroid("chmod +s " + authfsOutputDir + "/dir/dir2");
@@ -682,14 +687,14 @@
         // Action & Verify
         String authfsOutputDir = MOUNT_DIR + "/3";
         // Create with umask
-        runOnMicrodroid("umask 000; echo -n foo > " + authfsOutputDir + "/file");
-        runOnMicrodroid("umask 022; echo -n foo > " + authfsOutputDir + "/file2");
+        sMicrodroid.run("umask 000; echo -n foo > " + authfsOutputDir + "/file");
+        sMicrodroid.run("umask 022; echo -n foo > " + authfsOutputDir + "/file2");
         expectFileMode("-rw-rw-rw-", authfsOutputDir + "/file", TEST_OUTPUT_DIR + "/file");
         expectFileMode("-rw-r--r--", authfsOutputDir + "/file2", TEST_OUTPUT_DIR + "/file2");
         // Change mode
-        runOnMicrodroid("chmod -w " + authfsOutputDir + "/file");
+        sMicrodroid.run("chmod -w " + authfsOutputDir + "/file");
         expectFileMode("-r--r--r--", authfsOutputDir + "/file", TEST_OUTPUT_DIR + "/file");
-        runOnMicrodroid("chmod 321 " + authfsOutputDir + "/file2");
+        sMicrodroid.run("chmod 321 " + authfsOutputDir + "/file2");
         expectFileMode("--wx-w---x", authfsOutputDir + "/file2", TEST_OUTPUT_DIR + "/file2");
         // Can't set the disallowed bits
         assertFailedOnMicrodroid("chmod +s " + authfsOutputDir + "/file");
@@ -705,7 +710,16 @@
         // Verify
         // Magic matches. Has only 2 inodes (root and "/3").
         assertEquals(
-                FUSE_SUPER_MAGIC_HEX + " 2", runOnMicrodroid("stat -f -c '%t %c' " + MOUNT_DIR));
+                FUSE_SUPER_MAGIC_HEX + " 2", sMicrodroid.run("stat -f -c '%t %c' " + MOUNT_DIR));
+    }
+
+    private static File findTestApk(IBuildInfo buildInfo) {
+        try {
+            return (new CompatibilityBuildHelper(buildInfo)).getTestFile(TEST_APK_NAME);
+        } catch (FileNotFoundException e) {
+            fail("Missing test file: " + TEST_APK_NAME);
+            return null;
+        }
     }
 
     private void expectBackingFileConsistency(
@@ -719,8 +733,8 @@
                 "Inconsistent file hash on the backend storage", hashOnAuthFs, hashOfBackingFile);
     }
 
-    private String computeFileHashOnMicrodroid(String path) {
-        String result = runOnMicrodroid("sha256sum " + path);
+    private String computeFileHashOnMicrodroid(String path) throws DeviceNotAvailableException {
+        String result = sMicrodroid.run("sha256sum " + path);
         String[] tokens = result.split("\\s");
         if (tokens.length > 0) {
             return tokens[0];
@@ -735,7 +749,7 @@
         // TODO(b/182576497): cp returns error because close(2) returns ENOSYS in the current authfs
         // implementation. We should probably fix that since programs can expect close(2) return 0.
         String cmd = "cat " + src + " > " + dest;
-        return tryRunOnMicrodroid(cmd) != null;
+        return sMicrodroid.tryRun(cmd) != null;
     }
 
     private String computeFileHashOnAndroid(String path) throws DeviceNotAvailableException {
@@ -751,38 +765,42 @@
 
     private void expectFileMode(String expected, String microdroidPath, String androidPath)
             throws DeviceNotAvailableException {
-        String actual = runOnMicrodroid("stat -c '%A' " + microdroidPath);
+        String actual = sMicrodroid.run("stat -c '%A' " + microdroidPath);
         assertEquals("Inconsistent mode for " + microdroidPath, expected, actual);
 
         actual = sAndroid.run("stat -c '%A' " + androidPath);
         assertEquals("Inconsistent mode for " + androidPath + " (android)", expected, actual);
     }
 
-    private boolean resizeFileOnMicrodroid(String path, long size) {
-        CommandResult result = runOnMicrodroidForResult("truncate -c -s " + size + " " + path);
+    private boolean resizeFileOnMicrodroid(String path, long size)
+            throws DeviceNotAvailableException {
+        CommandResult result = sMicrodroid.runForResult("truncate -c -s " + size + " " + path);
         return result.getStatus() == CommandStatus.SUCCESS;
     }
 
-    private long getFileSizeInBytesOnMicrodroid(String path) {
-        return Long.parseLong(runOnMicrodroid("stat -c '%s' " + path));
+    private long getFileSizeInBytesOnMicrodroid(String path) throws DeviceNotAvailableException {
+        return Long.parseLong(sMicrodroid.run("stat -c '%s' " + path));
     }
 
-    private void createFileWithOnesOnMicrodroid(String filePath, long numberOfOnes) {
-        runOnMicrodroid(
+    private void createFileWithOnesOnMicrodroid(String filePath, long numberOfOnes)
+            throws DeviceNotAvailableException {
+        sMicrodroid.run(
                 "yes $'\\x01' | tr -d '\\n' | dd bs=1 count=" + numberOfOnes + " of=" + filePath);
     }
 
-    private boolean checkReadAtFileOffsetOnMicrodroid(String filePath, long offset, long size) {
+    private boolean checkReadAtFileOffsetOnMicrodroid(String filePath, long offset, long size)
+            throws DeviceNotAvailableException {
         String cmd = "dd if=" + filePath + " of=/dev/null bs=1 count=" + size;
         if (offset > 0) {
             cmd += " skip=" + offset;
         }
-        CommandResult result = runOnMicrodroidForResult(cmd);
+        CommandResult result = sMicrodroid.runForResult(cmd);
         return result.getStatus() == CommandStatus.SUCCESS;
     }
 
     private boolean writeZerosAtFileOffsetOnMicrodroid(
-            String filePath, long offset, long numberOfZeros, boolean writeThrough) {
+            String filePath, long offset, long numberOfZeros, boolean writeThrough)
+            throws DeviceNotAvailableException {
         String cmd = "dd if=/dev/zero of=" + filePath + " bs=1 count=" + numberOfZeros
                 + " conv=notrunc";
         if (offset > 0) {
@@ -791,7 +809,7 @@
         if (writeThrough) {
             cmd += " direct";
         }
-        CommandResult result = runOnMicrodroidForResult(cmd);
+        CommandResult result = sMicrodroid.runForResult(cmd);
         return result.getStatus() == CommandStatus.SUCCESS;
     }
 
@@ -810,9 +828,14 @@
                     // authfs may fail to start if fd_server is not yet listening on the vsock
                     // ("Error: Invalid raw AIBinder"). Just restart if that happens.
                     while (starting.get()) {
-                        CLog.i("Starting authfs");
-                        CommandResult result = runOnMicrodroidForResult(cmd);
-                        CLog.w("authfs has stopped: " + result);
+                        try {
+                            CLog.i("Starting authfs");
+                            CommandResult result = sMicrodroid.runForResult(cmd);
+                            CLog.w("authfs has stopped: " + result);
+                        } catch (DeviceNotAvailableException e) {
+                            CLog.e("Error running authfs", e);
+                            throw new RuntimeException(e);
+                        }
                     }
                 });
         try {
@@ -855,8 +878,8 @@
                 });
     }
 
-    private boolean isMicrodroidDirectoryOnFuse(String path) {
-        String fs_type = tryRunOnMicrodroid("stat -f -c '%t' " + path);
+    private boolean isMicrodroidDirectoryOnFuse(String path) throws DeviceNotAvailableException {
+        String fs_type = sMicrodroid.tryRun("stat -f -c '%t' " + path);
         return FUSE_SUPER_MAGIC_HEX.equals(fs_type);
     }
 }
diff --git a/microdroid/Android.bp b/microdroid/Android.bp
index 60f8fd4..b7d844f 100644
--- a/microdroid/Android.bp
+++ b/microdroid/Android.bp
@@ -79,6 +79,7 @@
         "tombstoned",
         "tombstone_transmit.microdroid",
         "cgroups.json",
+        "task_profiles.json",
         "public.libraries.android.txt",
 
         "microdroid_compatibility_matrix",
diff --git a/microdroid_manager/src/main.rs b/microdroid_manager/src/main.rs
index 8c85d3e..8a638db 100644
--- a/microdroid_manager/src/main.rs
+++ b/microdroid_manager/src/main.rs
@@ -146,17 +146,17 @@
     }
 }
 
-fn dice_derivation(verified_data: MicrodroidData, payload_config_path: &str) -> Result<()> {
+fn dice_derivation(verified_data: &MicrodroidData, payload_config_path: &str) -> Result<()> {
     // Calculate compound digests of code and authorities
     let mut code_hash_ctx = digest::Context::new(&digest::SHA512);
     let mut authority_hash_ctx = digest::Context::new(&digest::SHA512);
     code_hash_ctx.update(verified_data.apk_data.root_hash.as_ref());
     authority_hash_ctx.update(verified_data.apk_data.pubkey.as_ref());
-    for extra_apk in verified_data.extra_apks_data {
+    for extra_apk in &verified_data.extra_apks_data {
         code_hash_ctx.update(extra_apk.root_hash.as_ref());
         authority_hash_ctx.update(extra_apk.pubkey.as_ref());
     }
-    for apex in verified_data.apex_data {
+    for apex in &verified_data.apex_data {
         code_hash_ctx.update(apex.root_digest.as_ref());
         authority_hash_ctx.update(apex.public_key.as_ref());
     }
@@ -189,7 +189,7 @@
             authorityHash: authority_hash,
             authorityDescriptor: None,
             mode: if app_debuggable { Mode::DEBUG } else { Mode::NORMAL },
-            hidden: verified_data.salt.try_into().unwrap(),
+            hidden: verified_data.salt.clone().try_into().unwrap(),
         }])
         .context("IDiceMaintenance::demoteSelf failed")?;
     Ok(())
@@ -240,6 +240,10 @@
         instance.write_microdroid_data(&verified_data).context("Failed to write identity data")?;
     }
 
+    // To minimize the exposure to untrusted data, derive dice profile as soon as possible.
+    info!("DICE derivation for payload");
+    dice_derivation(&verified_data, &metadata.payload_config_path)?;
+
     // Before reading a file from the APK, start zipfuse
     run_zipfuse(
         "fscontext=u:object_r:zipfusefs:s0,context=u:object_r:system_file:s0",
@@ -270,9 +274,6 @@
     }
     mount_extra_apks(&config)?;
 
-    info!("DICE derivation for payload");
-    dice_derivation(verified_data, &metadata.payload_config_path)?;
-
     // Wait until apex config is done. (e.g. linker configuration for apexes)
     // TODO(jooyung): wait until sys.boot_completed?
     wait_for_apex_config_done()?;
diff --git a/pvmfw/Android.bp b/pvmfw/Android.bp
index fb1373d..fbdd8d7 100644
--- a/pvmfw/Android.bp
+++ b/pvmfw/Android.bp
@@ -12,6 +12,9 @@
         "libcompiler_builtins.rust_sysroot",
         "libcore.rust_sysroot",
     ],
+    rustlibs: [
+        "libspin_nostd",
+    ],
     enabled: false,
     target: {
         android_arm64: {
@@ -25,6 +28,7 @@
     name: "pvmfw",
     srcs: [
         "entry.S",
+        "idmap.S",
     ],
     static_libs: [
         "libpvmfw",
diff --git a/pvmfw/entry.S b/pvmfw/entry.S
index 25631cb..e5c6045 100644
--- a/pvmfw/entry.S
+++ b/pvmfw/entry.S
@@ -19,6 +19,60 @@
 	add \reg, \reg, :lo12:\sym
 .endm
 
+.macro mov_i, reg:req, imm:req
+	movz \reg, :abs_g3:\imm
+	movk \reg, :abs_g2_nc:\imm
+	movk \reg, :abs_g1_nc:\imm
+	movk \reg, :abs_g0_nc:\imm
+.endm
+
+.set .L_MAIR_DEV_nGnRE,	0x04
+.set .L_MAIR_MEM_WBWA,	0xff
+.set .Lmairval, .L_MAIR_DEV_nGnRE | (.L_MAIR_MEM_WBWA << 8)
+
+/* 4 KiB granule size for TTBR0_EL1. */
+.set .L_TCR_TG0_4KB, 0x0 << 14
+/* 4 KiB granule size for TTBR1_EL1. */
+.set .L_TCR_TG1_4KB, 0x2 << 30
+/* Disable translation table walk for TTBR1_EL1, generating a translation fault instead. */
+.set .L_TCR_EPD1, 0x1 << 23
+/* Translation table walks for TTBR0_EL1 are inner sharable. */
+.set .L_TCR_SH_INNER, 0x3 << 12
+/*
+ * Translation table walks for TTBR0_EL1 are outer write-back read-allocate write-allocate
+ * cacheable.
+ */
+.set .L_TCR_RGN_OWB, 0x1 << 10
+/*
+ * Translation table walks for TTBR0_EL1 are inner write-back read-allocate write-allocate
+ * cacheable.
+ */
+.set .L_TCR_RGN_IWB, 0x1 << 8
+/* Size offset for TTBR0_EL1 is 2**39 bytes (512 GiB). */
+.set .L_TCR_T0SZ_512, 64 - 39
+.set .Ltcrval, .L_TCR_TG0_4KB | .L_TCR_TG1_4KB | .L_TCR_EPD1 | .L_TCR_RGN_OWB
+.set .Ltcrval, .Ltcrval | .L_TCR_RGN_IWB | .L_TCR_SH_INNER | .L_TCR_T0SZ_512
+
+/* Stage 1 instruction access cacheability is unaffected. */
+.set .L_SCTLR_ELx_I, 0x1 << 12
+/* SP alignment fault if SP is not aligned to a 16 byte boundary. */
+.set .L_SCTLR_ELx_SA, 0x1 << 3
+/* Stage 1 data access cacheability is unaffected. */
+.set .L_SCTLR_ELx_C, 0x1 << 2
+/* EL0 and EL1 stage 1 MMU enabled. */
+.set .L_SCTLR_ELx_M, 0x1 << 0
+/* Privileged Access Never is unchanged on taking an exception to EL1. */
+.set .L_SCTLR_EL1_SPAN, 0x1 << 23
+/* All writable memory regions are treated as XN. */
+.set .L_SCTLR_EL1_WXN, 0x1 << 19
+/* SETEND instruction disabled at EL0 in aarch32 mode. */
+.set .L_SCTLR_EL1_SED, 0x1 << 8
+/* Various IT instructions are disabled at EL0 in aarch32 mode. */
+.set .L_SCTLR_EL1_ITD, 0x1 << 7
+.set .L_SCTLR_EL1_RES1, (0x1 << 11) | (0x1 << 20) | (0x1 << 22) | (0x1 << 28) | (0x1 << 29)
+.set .Lsctlrval, .L_SCTLR_ELx_M | .L_SCTLR_ELx_C | .L_SCTLR_ELx_SA | .L_SCTLR_EL1_ITD | .L_SCTLR_EL1_SED
+.set .Lsctlrval, .Lsctlrval | .L_SCTLR_ELx_I | .L_SCTLR_EL1_SPAN | .L_SCTLR_EL1_RES1 | .L_SCTLR_EL1_WXN
+
 /**
  * This is a generic entry point for an image. It carries out the operations
  * required to prepare the loaded image to be run. Specifically, it zeroes the
@@ -28,6 +82,41 @@
 .section .init.entry, "ax"
 .global entry
 entry:
+	/* Enable MMU and caches. */
+
+	/*
+	 * Load and apply the memory management configuration.
+	 */
+	adrp x1, idmap
+	mov_i x2, .Lmairval
+	mov_i x3, .Ltcrval
+	mov_i x4, .Lsctlrval
+
+	/* Copy the supported PA range into TCR_EL1.IPS. */
+	mrs x6, id_aa64mmfr0_el1
+	bfi x3, x6, #32, #4
+
+	msr ttbr0_el1, x1
+	msr mair_el1, x2
+	msr tcr_el1, x3
+
+	/*
+	 * Ensure everything before this point has completed, then invalidate any potentially stale
+	 * local TLB entries before they start being used.
+	 */
+	isb
+	tlbi vmalle1
+	ic iallu
+	dsb nsh
+	isb
+
+	/*
+	 * Configure sctlr_el1 to enable MMU and cache and don't proceed until
+	 * this has completed.
+	 */
+	msr sctlr_el1, x4
+	isb
+
 	/* Disable trapping floating point access in EL1. */
 	mrs x30, cpacr_el1
 	orr x30, x30, #(0x3 << 20)
@@ -42,13 +131,23 @@
 	stp xzr, xzr, [x29], #16
 	b 0b
 
-1:	/* Prepare the stack. */
-	adr x30, boot_stack_end
+1:	/* Copy the data section. */
+	adr_l x28, data_begin
+	adr_l x29, data_end
+	adr_l x30, data_lma
+2:	cmp x28, x29
+	b.ge 3f
+	ldp q0, q1, [x30], #32
+	stp q0, q1, [x28], #32
+	b 2b
+
+3:	/* Prepare the stack. */
+	adr_l x30, boot_stack_end
 	mov sp, x30
 
 	/* Call into Rust code. */
 	bl main
 
 	/* Loop forever waiting for interrupts. */
-2:	wfi
-	b 2b
+4:	wfi
+	b 4b
diff --git a/pvmfw/idmap.S b/pvmfw/idmap.S
new file mode 100644
index 0000000..f1df6cc
--- /dev/null
+++ b/pvmfw/idmap.S
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2022 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
+ *
+ *     https://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.
+ */
+
+.set .L_TT_TYPE_BLOCK, 0x1
+.set .L_TT_TYPE_PAGE,  0x3
+.set .L_TT_TYPE_TABLE, 0x3
+
+/* Access flag. */
+.set .L_TT_AF, 0x1 << 10
+/* Not global. */
+.set .L_TT_NG, 0x1 << 11
+.set .L_TT_RO, 0x2 << 6
+.set .L_TT_XN, 0x3 << 53
+
+.set .L_TT_MT_DEV, 0x0 << 2			// MAIR #0 (DEV_nGnRE)
+.set .L_TT_MT_MEM, (0x1 << 2) | (0x3 << 8)	// MAIR #1 (MEM_WBWA), inner shareable
+
+.set .L_BLOCK_RO,  .L_TT_TYPE_BLOCK | .L_TT_MT_MEM | .L_TT_AF | .L_TT_RO | .L_TT_XN
+.set .L_BLOCK_DEV, .L_TT_TYPE_BLOCK | .L_TT_MT_DEV | .L_TT_AF | .L_TT_XN
+.set .L_BLOCK_MEM, .L_TT_TYPE_BLOCK | .L_TT_MT_MEM | .L_TT_AF | .L_TT_XN | .L_TT_NG
+.set .L_BLOCK_MEM_XIP, .L_TT_TYPE_BLOCK | .L_TT_MT_MEM | .L_TT_AF | .L_TT_NG | .L_TT_RO
+
+.section ".rodata.idmap", "a", %progbits
+.global idmap
+.align 12
+idmap:
+	/* level 1 */
+	.quad		.L_BLOCK_DEV | 0x0		// 1 GB of device mappings
+	.quad		.L_BLOCK_DEV | 0x40000000	// Another 1 GB of device mapppings
+	.quad		.L_TT_TYPE_TABLE + 0f		// up to 1 GB of DRAM
+	.fill		509, 8, 0x0			// 509 GB of remaining VA space
+
+	/* level 2 */
+0:	.quad		.L_BLOCK_RO  | 0x80000000	// DT provided by VMM
+	.quad		.L_BLOCK_MEM_XIP | 0x80200000	// 2 MB of DRAM containing image
+	.quad		.L_BLOCK_MEM | 0x80400000	// 2 MB of writable DRAM
+	.fill		509, 8, 0x0
diff --git a/pvmfw/image.ld b/pvmfw/image.ld
index e08fbe2..4655f68 100644
--- a/pvmfw/image.ld
+++ b/pvmfw/image.ld
@@ -18,6 +18,7 @@
 {
 	dtb_region	: ORIGIN = 0x80000000, LENGTH = 2M
 	image		: ORIGIN = 0x80200000, LENGTH = 2M
+	writable_data	: ORIGIN = 0x80400000, LENGTH = 2M
 }
 
 /*
@@ -82,7 +83,9 @@
 		 */
 		. = ALIGN(32);
 		data_end = .;
-	} >image
+	} >writable_data AT>image
+	data_lma = LOADADDR(.data);
+
 	/* Everything beyond this point will not be included in the binary. */
 	bin_end = .;
 
@@ -93,14 +96,14 @@
 		*(COMMON)
 		. = ALIGN(16);
 		bss_end = .;
-	} >image
+	} >writable_data
 
 	.stack (NOLOAD) : ALIGN(4096) {
 		boot_stack_begin = .;
 		. += 40 * 4096;
 		. = ALIGN(4096);
 		boot_stack_end = .;
-	} >image
+	} >writable_data
 
 	/*
 	 * Remove unused sections from the image.
diff --git a/pvmfw/src/console.rs b/pvmfw/src/console.rs
new file mode 100644
index 0000000..b52d924
--- /dev/null
+++ b/pvmfw/src/console.rs
@@ -0,0 +1,112 @@
+// Copyright 2022, 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.
+
+//! Console driver for 8250 UART.
+
+use crate::uart::Uart;
+use core::fmt::{write, Arguments, Write};
+use spin::mutex::SpinMutex;
+
+const BASE_ADDRESS: usize = 0x3f8;
+
+static CONSOLE: SpinMutex<Option<Uart>> = SpinMutex::new(None);
+
+/// Initialises a new instance of the UART driver and returns it.
+fn create() -> Uart {
+    // Safe because BASE_ADDRESS is the base of the MMIO region for a UART and is mapped as device
+    // memory.
+    unsafe { Uart::new(BASE_ADDRESS) }
+}
+
+/// Initialises the global instance of the UART driver. This must be called before using
+/// the `print!` and `println!` macros.
+pub fn init() {
+    let uart = create();
+    CONSOLE.lock().replace(uart);
+}
+
+/// Writes a string to the console.
+///
+/// Panics if [`init`] was not called first.
+pub fn write_str(s: &str) {
+    CONSOLE.lock().as_mut().unwrap().write_str(s).unwrap();
+}
+
+/// Writes a formatted string to the console.
+///
+/// Panics if [`init`] was not called first.
+pub fn write_args(format_args: Arguments) {
+    write(CONSOLE.lock().as_mut().unwrap(), format_args).unwrap();
+}
+
+/// Reinitialises the UART driver and writes a string to it.
+///
+/// This is intended for use in situations where the UART may be in an unknown state or the global
+/// instance may be locked, such as in an exception handler or panic handler.
+pub fn emergency_write_str(s: &str) {
+    let mut uart = create();
+    let _ = uart.write_str(s);
+}
+
+/// Reinitialises the UART driver and writes a formatted string to it.
+///
+/// This is intended for use in situations where the UART may be in an unknown state or the global
+/// instance may be locked, such as in an exception handler or panic handler.
+pub fn emergency_write_args(format_args: Arguments) {
+    let mut uart = create();
+    let _ = write(&mut uart, format_args);
+}
+
+/// Prints the given string to the console.
+///
+/// Panics if the console has not yet been initialised. May hang if used in an exception context;
+/// use `eprint!` instead.
+#[macro_export]
+macro_rules! print {
+    ($($arg:tt)*) => ($crate::console::write_args(format_args!($($arg)*)));
+}
+
+/// Prints the given formatted string to the console, followed by a newline.
+///
+/// Panics if the console has not yet been initialised. May hang if used in an exception context;
+/// use `eprintln!` instead.
+#[macro_export]
+macro_rules! println {
+    () => ($crate::console::write_str("\n"));
+    ($($arg:tt)*) => ({
+        $crate::console::write_args(format_args!($($arg)*))};
+        $crate::console::write_str("\n");
+    );
+}
+
+/// Prints the given string to the console in an emergency, such as an exception handler.
+///
+/// Never panics.
+#[macro_export]
+macro_rules! eprint {
+    ($($arg:tt)*) => ($crate::console::emergency_write_args(format_args!($($arg)*)));
+}
+
+/// Prints the given string followed by a newline to the console in an emergency, such as an
+/// exception handler.
+///
+/// Never panics.
+#[macro_export]
+macro_rules! eprintln {
+    () => ($crate::console::emergency_write_str("\n"));
+    ($($arg:tt)*) => ({
+        $crate::console::emergency_write_args(format_args!($($arg)*))};
+        $crate::console::emergency_write_str("\n");
+    );
+}
diff --git a/pvmfw/src/main.rs b/pvmfw/src/main.rs
index 0a359f6..4ab14b7 100644
--- a/pvmfw/src/main.rs
+++ b/pvmfw/src/main.rs
@@ -17,7 +17,9 @@
 #![no_main]
 #![no_std]
 
+mod console;
 mod psci;
+mod uart;
 
 use core::panic::PanicInfo;
 use psci::{system_off, system_reset};
@@ -25,13 +27,17 @@
 /// Entry point for pVM firmware.
 #[no_mangle]
 pub extern "C" fn main() -> ! {
+    console::init();
+    println!("Hello world");
+
     system_off();
     #[allow(clippy::empty_loop)]
     loop {}
 }
 
 #[panic_handler]
-fn panic(_info: &PanicInfo) -> ! {
+fn panic(info: &PanicInfo) -> ! {
+    eprintln!("{}", info);
     system_reset();
     loop {}
 }
diff --git a/pvmfw/src/uart.rs b/pvmfw/src/uart.rs
new file mode 100644
index 0000000..0fc2494
--- /dev/null
+++ b/pvmfw/src/uart.rs
@@ -0,0 +1,59 @@
+// Copyright 2022, 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.
+
+//! Minimal driver for an 8250 UART. This only implements enough to work with the emulated 8250
+//! provided by crosvm, and won't work with real hardware.
+
+use core::fmt::{self, Write};
+use core::ptr::write_volatile;
+
+/// Minimal driver for an 8250 UART. This only implements enough to work with the emulated 8250
+/// provided by crosvm, and won't work with real hardware.
+pub struct Uart {
+    base_address: *mut u8,
+}
+
+impl Uart {
+    /// Constructs a new instance of the UART driver for a device at the given base address.
+    ///
+    /// # Safety
+    ///
+    /// The given base address must point to the 8 MMIO control registers of an appropriate UART
+    /// device, which must be mapped into the address space of the process as device memory and not
+    /// have any other aliases.
+    pub unsafe fn new(base_address: usize) -> Self {
+        Self { base_address: base_address as *mut u8 }
+    }
+
+    /// Writes a single byte to the UART.
+    pub fn write_byte(&self, byte: u8) {
+        // Safe because we know that the base address points to the control registers of an UART
+        // device which is appropriately mapped.
+        unsafe {
+            write_volatile(self.base_address, byte);
+        }
+    }
+}
+
+impl Write for Uart {
+    fn write_str(&mut self, s: &str) -> fmt::Result {
+        for c in s.as_bytes() {
+            self.write_byte(*c);
+        }
+        Ok(())
+    }
+}
+
+// Safe because it just contains a pointer to device memory, which can be accessed from any context.
+unsafe impl Send for Uart {}
diff --git a/tests/hostside/java/android/virt/test/MicrodroidTestCase.java b/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
index 7944245..32bdf3b 100644
--- a/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
+++ b/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
@@ -153,7 +153,9 @@
         command.add(virtApexDir.getPath());
 
         CommandResult result = runUtil.runTimedCmd(
-                                    20 * 1000,
+                                    // sign_virt_apex is so slow on CI server that this often times
+                                    // out. Until we can make it fast, use 50s for timeout
+                                    50 * 1000,
                                     "/bin/bash",
                                     "-c",
                                     String.join(" ", command));