Merge "microdroid_manager: Fix duplicate error log"
diff --git a/.gitignore b/.gitignore
index 96ef6c0..5917dfb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,5 @@
-/target
+apkdmverity/target/
+zipfuse/target/
+policy
+zipfuse/target/
 Cargo.lock
diff --git a/OWNERS b/OWNERS
index 7e479c4..5bd97f3 100644
--- a/OWNERS
+++ b/OWNERS
@@ -12,6 +12,7 @@
 ardb@google.com
 ascull@google.com
 inseob@google.com
+jeffv@google.com
 jooyung@google.com
 mzyngier@google.com
 ptosi@google.com
diff --git a/apkdmverity/src/main.rs b/apkdmverity/src/main.rs
index 16dd480..de7f5bb 100644
--- a/apkdmverity/src/main.rs
+++ b/apkdmverity/src/main.rs
@@ -45,7 +45,7 @@
                             block device is created at \"/dev/mapper/<name>\".' root_hash is \
                             optional; idsig file's root hash will be used if specified as \"none\"."
             ))
-        .arg(Arg::with_name("verbose").short("v").long("verbose").help("Shows verbose output"))
+        .arg(Arg::with_name("verbose").short('v').long("verbose").help("Shows verbose output"))
         .get_matches();
 
     let apks = matches.values_of("apk").unwrap();
diff --git a/authfs/Android.bp b/authfs/Android.bp
index 40643b8..cb7f119 100644
--- a/authfs/Android.bp
+++ b/authfs/Android.bp
@@ -14,7 +14,7 @@
         "libandroid_logger",
         "libanyhow",
         "libauthfs_fsverity_metadata",
-        "libbinder_rpc_unstable_bindgen",
+        "libbinder_common",
         "libbinder_rs",
         "libcfg_if",
         "libfsverity_digests_proto_rust",
diff --git a/authfs/fd_server/Android.bp b/authfs/fd_server/Android.bp
index 9499cd2..943eec1 100644
--- a/authfs/fd_server/Android.bp
+++ b/authfs/fd_server/Android.bp
@@ -11,7 +11,6 @@
         "libanyhow",
         "libauthfs_fsverity_metadata",
         "libbinder_common",
-        "libbinder_rpc_unstable_bindgen",
         "libbinder_rs",
         "libclap",
         "liblibc",
diff --git a/authfs/src/file.rs b/authfs/src/file.rs
index 44e60d8..d9f8964 100644
--- a/authfs/src/file.rs
+++ b/authfs/src/file.rs
@@ -6,15 +6,14 @@
 pub use dir::{InMemoryDir, RemoteDirEditor};
 pub use remote_file::{RemoteFileEditor, RemoteFileReader, RemoteMerkleTreeReader};
 
-use binder::unstable_api::{new_spibinder, AIBinder};
-use binder::FromIBinder;
-use std::convert::TryFrom;
-use std::io;
-use std::path::{Path, MAIN_SEPARATOR};
-
 use crate::common::{divide_roundup, CHUNK_SIZE};
 use authfs_aidl_interface::aidl::com::android::virt::fs::IVirtFdService::IVirtFdService;
 use authfs_aidl_interface::binder::{Status, Strong};
+use binder::StatusCode;
+use binder_common::rpc_client::connect_rpc_binder;
+use std::convert::TryFrom;
+use std::io;
+use std::path::{Path, MAIN_SEPARATOR};
 
 pub type VirtFdService = Strong<dyn IVirtFdService>;
 pub type VirtFdServiceStatus = Status;
@@ -24,21 +23,15 @@
 pub const RPC_SERVICE_PORT: u32 = 3264;
 
 pub fn get_rpc_binder_service(cid: u32) -> io::Result<VirtFdService> {
-    // SAFETY: AIBinder returned by RpcClient has correct reference count, and the ownership can be
-    // safely taken by new_spibinder.
-    let ibinder = unsafe {
-        new_spibinder(binder_rpc_unstable_bindgen::RpcClient(cid, RPC_SERVICE_PORT) as *mut AIBinder)
-    };
-    if let Some(ibinder) = ibinder {
-        Ok(<dyn IVirtFdService>::try_from(ibinder).map_err(|e| {
-            io::Error::new(
-                io::ErrorKind::AddrNotAvailable,
-                format!("Cannot connect to RPC service: {}", e),
-            )
-        })?)
-    } else {
-        Err(io::Error::new(io::ErrorKind::InvalidInput, "Invalid raw AIBinder"))
-    }
+    connect_rpc_binder(cid, RPC_SERVICE_PORT).map_err(|e| match e {
+        StatusCode::BAD_VALUE => {
+            io::Error::new(io::ErrorKind::InvalidInput, "Invalid raw AIBinder")
+        }
+        _ => io::Error::new(
+            io::ErrorKind::AddrNotAvailable,
+            format!("Cannot connect to RPC service: {}", e),
+        ),
+    })
 }
 
 /// A trait for reading data by chunks. Chunks can be read by specifying the chunk index. Only the
diff --git a/avmd/src/avmd.rs b/avmd/src/avmd.rs
index e3bc7a7..50cdfdf 100644
--- a/avmd/src/avmd.rs
+++ b/avmd/src/avmd.rs
@@ -12,9 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+extern crate alloc;
+
+use alloc::{
+    string::{String, ToString},
+    vec::Vec,
+};
 use apexutil::to_hex_string;
+use core::fmt;
 use serde::{Deserialize, Serialize};
-use std::fmt;
 
 /// An Avmd struct contains
 /// - A header with version information that allows rollback when needed.
diff --git a/avmd/src/lib.rs b/avmd/src/lib.rs
index 9722518..7a06e6a 100644
--- a/avmd/src/lib.rs
+++ b/avmd/src/lib.rs
@@ -14,6 +14,8 @@
 
 //! Library for handling AVMD blobs.
 
+#![no_std]
+
 mod avmd;
 
 pub use avmd::{ApkDescriptor, Avmd, Descriptor, ResourceIdentifier, VbMetaDescriptor};
diff --git a/avmd/src/main.rs b/avmd/src/main.rs
index b156a66..ca28f42 100644
--- a/avmd/src/main.rs
+++ b/avmd/src/main.rs
@@ -149,8 +149,8 @@
 
     let args = app.get_matches();
     match args.subcommand() {
-        ("create", Some(sub_args)) => create(sub_args)?,
-        ("dump", Some(sub_args)) => dump(sub_args)?,
+        Some(("create", sub_args)) => create(sub_args)?,
+        Some(("dump", sub_args)) => dump(sub_args)?,
         _ => bail!("Invalid arguments"),
     }
     Ok(())
diff --git a/compos/Android.bp b/compos/Android.bp
index 69b22d6..0f1675b 100644
--- a/compos/Android.bp
+++ b/compos/Android.bp
@@ -12,7 +12,6 @@
         "libandroid_logger",
         "libanyhow",
         "libbinder_common",
-        "libbinder_rpc_unstable_bindgen",
         "libbinder_rs",
         "libclap",
         "libcompos_common",
diff --git a/compos/common/Android.bp b/compos/common/Android.bp
index 1a69b1a..3c3397d 100644
--- a/compos/common/Android.bp
+++ b/compos/common/Android.bp
@@ -12,7 +12,6 @@
         "compos_aidl_interface-rust",
         "libanyhow",
         "libbinder_common",
-        "libbinder_rpc_unstable_bindgen",
         "liblazy_static",
         "liblog_rust",
         "libnested_virt",
diff --git a/compos/composd_cmd/composd_cmd.rs b/compos/composd_cmd/composd_cmd.rs
index c6a5479..d5feed8 100644
--- a/compos/composd_cmd/composd_cmd.rs
+++ b/compos/composd_cmd/composd_cmd.rs
@@ -49,8 +49,8 @@
     ProcessState::start_thread_pool();
 
     match args.subcommand() {
-        ("staged-apex-compile", _) => run_staged_apex_compile()?,
-        ("test-compile", Some(sub_matches)) => {
+        Some(("staged-apex-compile", _)) => run_staged_apex_compile()?,
+        Some(("test-compile", sub_matches)) => {
             let prefer_staged = sub_matches.is_present("prefer-staged");
             run_test_compile(prefer_staged)?;
         }
diff --git a/compos/src/compsvc_main.rs b/compos/src/compsvc_main.rs
index 4ecbfe9..186977e 100644
--- a/compos/src/compsvc_main.rs
+++ b/compos/src/compsvc_main.rs
@@ -28,12 +28,8 @@
     },
     binder::Strong,
 };
-use anyhow::{anyhow, bail, Context, Result};
-use binder::{
-    unstable_api::{new_spibinder, AIBinder},
-    FromIBinder,
-};
-use binder_common::rpc_server::run_rpc_server;
+use anyhow::{bail, Context, Result};
+use binder_common::{rpc_client::connect_rpc_binder, rpc_server::run_rpc_server};
 use compos_common::COMPOS_VSOCK_PORT;
 use log::{debug, error};
 use std::panic;
@@ -76,15 +72,6 @@
 }
 
 fn get_vm_service() -> Result<Strong<dyn IVirtualMachineService>> {
-    // SAFETY: AIBinder returned by RpcClient has correct reference count, and the ownership
-    // can be safely taken by new_spibinder.
-    let ibinder = unsafe {
-        new_spibinder(binder_rpc_unstable_bindgen::RpcClient(
-            VMADDR_CID_HOST,
-            VM_BINDER_SERVICE_PORT as u32,
-        ) as *mut AIBinder)
-    }
-    .ok_or_else(|| anyhow!("Failed to connect to IVirtualMachineService"))?;
-
-    FromIBinder::try_from(ibinder).context("Connecting to IVirtualMachineService")
+    connect_rpc_binder(VMADDR_CID_HOST, VM_BINDER_SERVICE_PORT as u32)
+        .context("Connecting to IVirtualMachineService")
 }
diff --git a/microdroid_manager/Android.bp b/microdroid_manager/Android.bp
index d1afc14..3ba2700 100644
--- a/microdroid_manager/Android.bp
+++ b/microdroid_manager/Android.bp
@@ -16,7 +16,7 @@
         "libanyhow",
         "libapexutil_rust",
         "libapkverify",
-        "libbinder_rpc_unstable_bindgen",
+        "libbinder_common",
         "libbinder_rs",
         "libbyteorder",
         "libdiced_utils",
diff --git a/microdroid_manager/src/main.rs b/microdroid_manager/src/main.rs
index 557a379..fa064a7 100644
--- a/microdroid_manager/src/main.rs
+++ b/microdroid_manager/src/main.rs
@@ -25,8 +25,8 @@
 use android_security_dice::aidl::android::security::dice::IDiceMaintenance::IDiceMaintenance;
 use anyhow::{anyhow, bail, ensure, Context, Error, Result};
 use apkverify::{get_public_key_der, verify};
-use binder::unstable_api::{new_spibinder, AIBinder};
-use binder::{wait_for_interface, FromIBinder, Strong};
+use binder::{wait_for_interface, Strong};
+use binder_common::rpc_client::connect_rpc_binder;
 use diced_utils::cbor::encode_header;
 use glob::glob;
 use idsig::V4Signature;
@@ -139,19 +139,8 @@
 }
 
 fn get_vms_rpc_binder() -> Result<Strong<dyn IVirtualMachineService>> {
-    // SAFETY: AIBinder returned by RpcClient has correct reference count, and the ownership can be
-    // safely taken by new_spibinder.
-    let ibinder = unsafe {
-        new_spibinder(binder_rpc_unstable_bindgen::RpcClient(
-            VMADDR_CID_HOST,
-            VM_BINDER_SERVICE_PORT as u32,
-        ) as *mut AIBinder)
-    };
-    if let Some(ibinder) = ibinder {
-        <dyn IVirtualMachineService>::try_from(ibinder).context("Cannot connect to RPC service")
-    } else {
-        bail!("Invalid raw AIBinder")
-    }
+    connect_rpc_binder(VMADDR_CID_HOST, VM_BINDER_SERVICE_PORT as u32)
+        .context("Cannot connect to RPC service")
 }
 
 fn main() -> Result<()> {
diff --git a/tests/aidl/com/android/microdroid/testservice/IBenchmarkService.aidl b/tests/aidl/com/android/microdroid/testservice/IBenchmarkService.aidl
new file mode 100644
index 0000000..afcf989
--- /dev/null
+++ b/tests/aidl/com/android/microdroid/testservice/IBenchmarkService.aidl
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+
+package com.android.microdroid.testservice;
+
+/** {@hide} */
+interface IBenchmarkService {
+    const int SERVICE_PORT = 5677;
+
+    /** Reads a file and returns the elapsed seconds for the reading. */
+    double readFile(String filename, long fileSizeBytes, boolean isRand);
+}
diff --git a/tests/benchmark/Android.bp b/tests/benchmark/Android.bp
index e6d5b83..2111620 100644
--- a/tests/benchmark/Android.bp
+++ b/tests/benchmark/Android.bp
@@ -12,6 +12,7 @@
         "MicroroidDeviceTestHelper",
         "androidx.test.runner",
         "androidx.test.ext.junit",
+        "com.android.microdroid.testservice-java",
         "truth-prebuilt",
     ],
     libs: ["android.system.virtualmachine"],
@@ -24,4 +25,12 @@
 cc_library_shared {
     name: "MicrodroidBenchmarkNativeLib",
     srcs: ["src/native/benchmarkbinary.cpp"],
+    shared_libs: [
+        "android.system.virtualmachineservice-ndk",
+        "com.android.microdroid.testservice-ndk",
+        "libbase",
+        "libbinder_ndk",
+        "libbinder_rpc_unstable",
+        "liblog",
+    ],
 }
diff --git a/tests/benchmark/assets/vm_config.json b/tests/benchmark/assets/vm_config.json
index 67e3d21..e8f43e0 100644
--- a/tests/benchmark/assets/vm_config.json
+++ b/tests/benchmark/assets/vm_config.json
@@ -4,7 +4,10 @@
   },
   "task": {
     "type": "microdroid_launcher",
-    "command": "MicrodroidBenchmarkNativeLib.so"
+    "command": "MicrodroidBenchmarkNativeLib.so",
+    "args": [
+      "no_io"
+    ]
   },
   "export_tombstones": true
 }
diff --git a/tests/benchmark/assets/vm_config_io.json b/tests/benchmark/assets/vm_config_io.json
new file mode 100644
index 0000000..1a5a9e5
--- /dev/null
+++ b/tests/benchmark/assets/vm_config_io.json
@@ -0,0 +1,18 @@
+{
+  "os": {
+    "name": "microdroid"
+  },
+  "task": {
+    "type": "microdroid_launcher",
+    "command": "MicrodroidBenchmarkNativeLib.so",
+    "args": [
+      "io"
+    ]
+  },
+  "apexes": [
+    {
+      "name": "com.android.virt"
+    }
+  ],
+  "export_tombstones": true
+}
diff --git a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
index 3731e13..7ee2d39 100644
--- a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
+++ b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
@@ -13,6 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package com.android.microdroid.benchmark;
 
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
@@ -22,11 +23,13 @@
 
 import android.app.Instrumentation;
 import android.os.Bundle;
+import android.system.virtualmachine.VirtualMachine;
 import android.system.virtualmachine.VirtualMachineConfig;
 import android.system.virtualmachine.VirtualMachineConfig.DebugLevel;
 import android.system.virtualmachine.VirtualMachineException;
 
 import com.android.microdroid.test.MicrodroidDeviceTestBase;
+import com.android.microdroid.testservice.IBenchmarkService;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -38,10 +41,13 @@
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.List;
 
 @RunWith(Parameterized.class)
 public class MicrodroidBenchmarks extends MicrodroidDeviceTestBase {
     private static final String TAG = "MicrodroidBenchmarks";
+    private static final int VIRTIO_BLK_TRIAL_COUNT = 5;
 
     @Rule public Timeout globalTimeout = Timeout.seconds(300);
 
@@ -159,12 +165,100 @@
                 continue;
             }
 
-            String base = name.substring(MICRODROID_IMG_PREFIX.length(),
-                                         name.length() - MICRODROID_IMG_SUFFIX.length());
-            String metric = "avf_perf/microdroid/img_size_" + base + "_MB";
+            String base =
+                    name.substring(
+                            MICRODROID_IMG_PREFIX.length(),
+                            name.length() - MICRODROID_IMG_SUFFIX.length());
+            String metric = "avf_perf/microdroid/img_size_" + base + "_MB" + "+" + name;
             double size = Files.size(file.toPath()) / SIZE_MB;
             bundle.putDouble(metric, size);
         }
         mInstrumentation.sendStatus(0, bundle);
     }
+
+    @Test
+    public void testVirtioBlkSeqReadRate() throws Exception {
+        testVirtioBlkReadRate(/*isRand=*/ false);
+    }
+
+    @Test
+    public void testVirtioBlkRandReadRate() throws Exception {
+        testVirtioBlkReadRate(/*isRand=*/ true);
+    }
+
+    private void testVirtioBlkReadRate(boolean isRand) throws Exception {
+        VirtualMachineConfig.Builder builder =
+                mInner.newVmConfigBuilder("assets/vm_config_io.json");
+        VirtualMachineConfig config = builder.debugLevel(DebugLevel.FULL).build();
+        List<Double> readRates = new ArrayList<>();
+
+        for (int i = 0; i < VIRTIO_BLK_TRIAL_COUNT; ++i) {
+            String vmName = "test_vm_io_" + i;
+            mInner.forceCreateNewVirtualMachine(vmName, config);
+            VirtualMachine vm = mInner.getVirtualMachineManager().get(vmName);
+            VirtioBlkVmEventListener listener = new VirtioBlkVmEventListener(readRates, isRand);
+            listener.runToFinish(TAG, vm);
+        }
+        reportMetrics(readRates, isRand);
+    }
+
+    private void reportMetrics(List<Double> readRates, boolean isRand) {
+        double sum = 0;
+        for (double rate : readRates) {
+            sum += rate;
+        }
+        double mean = sum / readRates.size();
+        double sqSum = 0;
+        for (double rate : readRates) {
+            sqSum += (rate - mean) * (rate - mean);
+        }
+        double stdDev = Math.sqrt(sqSum / (readRates.size() - 1));
+
+        Bundle bundle = new Bundle();
+        String metricNamePrefix =
+                "avf_perf/virtio-blk/"
+                        + (mProtectedVm ? "protected-vm/" : "unprotected-vm/")
+                        + (isRand ? "rand_read_" : "seq_read_");
+        String unit = "_mb_per_sec";
+
+        bundle.putDouble(metricNamePrefix + "mean" + unit, mean);
+        bundle.putDouble(metricNamePrefix + "std" + unit, stdDev);
+        mInstrumentation.sendStatus(0, bundle);
+    }
+
+    private static class VirtioBlkVmEventListener extends VmEventListener {
+        private static final String FILENAME = APEX_ETC_FS + "microdroid_super.img";
+
+        private final long mFileSizeBytes;
+        private final List<Double> mReadRates;
+        private final boolean mIsRand;
+
+        VirtioBlkVmEventListener(List<Double> readRates, boolean isRand) {
+            File file = new File(FILENAME);
+            try {
+                mFileSizeBytes = Files.size(file.toPath());
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+            assertThat(mFileSizeBytes).isGreaterThan((long) SIZE_MB);
+            mReadRates = readRates;
+            mIsRand = isRand;
+        }
+
+        @Override
+        public void onPayloadReady(VirtualMachine vm) {
+            try {
+                IBenchmarkService benchmarkService =
+                        IBenchmarkService.Stub.asInterface(
+                                vm.connectToVsockServer(IBenchmarkService.SERVICE_PORT).get());
+                double elapsedSeconds =
+                        benchmarkService.readFile(FILENAME, mFileSizeBytes, mIsRand);
+                double fileSizeMb = mFileSizeBytes / SIZE_MB;
+                mReadRates.add(fileSizeMb / elapsedSeconds);
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            }
+            forceStop(vm);
+        }
+    }
 }
diff --git a/tests/benchmark/src/native/benchmarkbinary.cpp b/tests/benchmark/src/native/benchmarkbinary.cpp
index b5ec49c..5523579 100644
--- a/tests/benchmark/src/native/benchmarkbinary.cpp
+++ b/tests/benchmark/src/native/benchmarkbinary.cpp
@@ -13,11 +13,123 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+#include <aidl/android/system/virtualmachineservice/IVirtualMachineService.h>
+#include <aidl/com/android/microdroid/testservice/BnBenchmarkService.h>
+#include <android-base/result.h>
+#include <android-base/unique_fd.h>
+#include <fcntl.h>
+#include <linux/vm_sockets.h>
+#include <stdio.h>
 #include <unistd.h>
 
-extern "C" int android_native_main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) {
-    // do nothing for now; just leave it alive. good night.
-    for (;;) {
-        sleep(1000);
+#include <binder_rpc_unstable.hpp>
+#include <chrono>
+#include <random>
+#include <string>
+
+#include "android-base/logging.h"
+
+using aidl::android::system::virtualmachineservice::IVirtualMachineService;
+using android::base::ErrnoError;
+using android::base::Error;
+using android::base::Result;
+using android::base::unique_fd;
+
+namespace {
+constexpr uint64_t kBlockSizeBytes = 4096;
+
+class IOBenchmarkService : public aidl::com::android::microdroid::testservice::BnBenchmarkService {
+public:
+    ndk::ScopedAStatus readFile(const std::string& filename, int64_t fileSizeBytes, bool isRand,
+                                double* out) override {
+        if (auto res = read_file(filename, fileSizeBytes, isRand); res.ok()) {
+            *out = res.value();
+        } else {
+            std::stringstream error;
+            error << "Failed reading file: " << res.error();
+            return ndk::ScopedAStatus::fromExceptionCodeWithMessage(EX_ILLEGAL_ARGUMENT,
+                                                                    error.str().c_str());
+        }
+        return ndk::ScopedAStatus::ok();
     }
+
+private:
+    /** Returns the elapsed seconds for reading the file. */
+    Result<double> read_file(const std::string& filename, int64_t fileSizeBytes, bool is_rand) {
+        const int64_t block_count = fileSizeBytes / kBlockSizeBytes;
+        std::vector<uint64_t> offsets;
+        if (is_rand) {
+            std::mt19937 rd{std::random_device{}()};
+            offsets.reserve(block_count);
+            for (auto i = 0; i < block_count; ++i) offsets.push_back(i * kBlockSizeBytes);
+            std::shuffle(offsets.begin(), offsets.end(), rd);
+        }
+        char buf[kBlockSizeBytes];
+
+        clock_t start = clock();
+        unique_fd fd(open(filename.c_str(), O_RDONLY | O_CLOEXEC));
+        if (fd.get() == -1) {
+            return ErrnoError() << "Read: opening " << filename << " failed";
+        }
+        for (auto i = 0; i < block_count; ++i) {
+            if (is_rand) {
+                if (lseek(fd.get(), offsets[i], SEEK_SET) == -1) {
+                    return ErrnoError() << "failed to lseek";
+                }
+            }
+            auto bytes = read(fd.get(), buf, kBlockSizeBytes);
+            if (bytes == 0) {
+                return Error() << "unexpected end of file";
+            } else if (bytes == -1) {
+                return ErrnoError() << "failed to read";
+            }
+        }
+        return {((double)clock() - start) / CLOCKS_PER_SEC};
+    }
+};
+
+Result<void> run_io_benchmark_tests() {
+    auto test_service = ndk::SharedRefBase::make<IOBenchmarkService>();
+    auto callback = []([[maybe_unused]] void* param) {
+        // Tell microdroid_manager that we're ready.
+        // If we can't, abort in order to fail fast - the host won't proceed without
+        // receiving the onReady signal.
+        ndk::SpAIBinder binder(
+                RpcClient(VMADDR_CID_HOST, IVirtualMachineService::VM_BINDER_SERVICE_PORT));
+        auto vm_service = IVirtualMachineService::fromBinder(binder);
+        if (vm_service == nullptr) {
+            LOG(ERROR) << "failed to connect VirtualMachineService\n";
+            abort();
+        }
+        if (auto status = vm_service->notifyPayloadReady(); !status.isOk()) {
+            LOG(ERROR) << "failed to notify payload ready to virtualizationservice: "
+                       << status.getDescription();
+            abort();
+        }
+    };
+
+    if (!RunRpcServerCallback(test_service->asBinder().get(), test_service->SERVICE_PORT, callback,
+                              nullptr)) {
+        return Error() << "RPC Server failed to run";
+    }
+    return {};
+}
+} // Anonymous namespace
+
+extern "C" int android_native_main([[maybe_unused]] int argc, char* argv[]) {
+    if (strcmp(argv[1], "no_io") == 0) {
+        // do nothing for now; just leave it alive. good night.
+        for (;;) {
+            sleep(1000);
+        }
+    } else if (strcmp(argv[1], "io") == 0) {
+        if (auto res = run_io_benchmark_tests(); res.ok()) {
+            return 0;
+        } else {
+            LOG(ERROR) << "IO benchmark test failed: " << res.error() << "\n";
+            return 1;
+        }
+    }
+    return 0;
 }
diff --git a/tests/helper/src/java/com/android/microdroid/test/MicrodroidDeviceTestBase.java b/tests/helper/src/java/com/android/microdroid/test/MicrodroidDeviceTestBase.java
index a2c43d7..1f57634 100644
--- a/tests/helper/src/java/com/android/microdroid/test/MicrodroidDeviceTestBase.java
+++ b/tests/helper/src/java/com/android/microdroid/test/MicrodroidDeviceTestBase.java
@@ -139,7 +139,7 @@
     protected abstract static class VmEventListener implements VirtualMachineCallback {
         private ExecutorService mExecutorService = Executors.newSingleThreadExecutor();
 
-        void runToFinish(String logTag, VirtualMachine vm)
+        public void runToFinish(String logTag, VirtualMachine vm)
                 throws VirtualMachineException, InterruptedException {
             vm.setCallback(mExecutorService, this);
             vm.run();
@@ -148,7 +148,7 @@
             mExecutorService.awaitTermination(300, TimeUnit.SECONDS);
         }
 
-        void forceStop(VirtualMachine vm) {
+        protected void forceStop(VirtualMachine vm) {
             try {
                 vm.clearCallback();
                 vm.stop();
diff --git a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
index 1141106..b429e4d 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -157,6 +157,26 @@
     }
 
     @Test
+    public void bootFailsWhenLowMem() throws VirtualMachineException, InterruptedException {
+        VirtualMachineConfig lowMemConfig = mInner.newVmConfigBuilder("assets/vm_config.json")
+                .memoryMib(20)
+                .debugLevel(DebugLevel.NONE)
+                .build();
+        VirtualMachine vm = mInner.forceCreateNewVirtualMachine("low_mem", lowMemConfig);
+        final CompletableFuture<Integer> exception = new CompletableFuture<>();
+        VmEventListener listener =
+                new VmEventListener() {
+                    @Override
+                    public void onDied(VirtualMachine vm, @DeathReason int reason) {
+                        exception.complete(reason);
+                        super.onDied(vm, reason);
+                    }
+                };
+        listener.runToFinish(TAG, vm);
+        assertThat(exception.getNow(0)).isAnyOf(DeathReason.REBOOT, DeathReason.HANGUP);
+    }
+
+    @Test
     public void changingDebugLevelInvalidatesVmIdentity()
             throws VirtualMachineException, InterruptedException, IOException {
         assume()
diff --git a/zipfuse/src/main.rs b/zipfuse/src/main.rs
index 874056a..8400a72 100644
--- a/zipfuse/src/main.rs
+++ b/zipfuse/src/main.rs
@@ -41,7 +41,7 @@
     let matches = App::new("zipfuse")
         .arg(
             Arg::with_name("options")
-                .short("o")
+                .short('o')
                 .takes_value(true)
                 .required(false)
                 .help("Comma separated list of mount options"),