Microdroid

Microdroid is a mini-Android OS that runs in a pVM. You don't have to use Microdroid, you can start a VM with any OS. However, the primary use cases for pVMs aren't running a standalone OS but rather offering an isolated execution environment for running a portion of an app with stronger confidentiality and integrity guarantees than Android can provide.

With traditional operating systems, providing strong confidentiality and integrity requires a fair amount of work (often duplicated) because traditional operating systems don't fit with the overarching Android architecture. For example, with the standard Android architecture, developers need to implement a means of securely loading and executing part of their app in the pVM, and the payload is built against glibc. The Android app uses Bionic, communication requires a custom protocol over vsock, and debugging using adb is challenging.

Microdroid fills these gaps by providing an off-the-shelf OS image designed to require the least amount of effort from developers to offload a portion of their app into a pVM. Native code is built against Bionic, communication happens over Binder, and it allows importing APEXes from the Android and exposes a subset of the Android API, such as keystore for cryptographic operations with hardware-backed keys. Overall, developers should find Microdroid a familiar environment with the tools they’ve grown accustomed in the full Android OS.

Features

Microdroid is a stripped down version of Android with a few additional components specific to pVMs. Microdroid supports:

  • A subset of NDK APIs (all APIs for Android's implementation of libc and Bionic are provided)
  • Debugging features, such as adb, logcat, tombstone, and gdb
  • Verified Boot and SELinux enabled
  • Loading and executing a binary, together with shared libraries, embedded in an APK
  • Binder RPC over vsock and exchange of files with implicit integrity checks
  • Loading of APEXes

Microdroid doesn't support:

  • Android Java APIs in the android.\* packages

  • SystemServer and Zygote

  • Graphics/UI

  • HALs

Microdroid architecture

Microdroid is similar to Cuttlefish in that both have an architecture that's similar to standard Android. Microdroid consists of the following partition images grouped together in a composite disk image:

  • bootloader - Verifies and starts the kernel.
  • boot.img - Contains the kernel and init ramdisk.
  • vendor_boot.img - Contains VM-specific kernel modules, such as virtio.
  • super.img - Consists of system and vendor logical partitions.
  • vbmeta.img - Contains verified boot metadata.

The partition images ship in the Virtualization APEX and are packaged in a composite disk image by VirtualizationService. In addition to the main OS composite disk image, VirtualizationService is responsible for creating these other partitions:

  • payload - A set of partitions backed by Android’s APEXes and APKs
  • instance - An encrypted partition for persisting per-instance verified boot data, such as per-instance salt, trusted APEX public keys, and rollback counters

Boot sequence

The Microdroid boot sequence occurs after Device boot. Device boot is discussed in the of the Architecture document. Figure 1 shows the steps that take place during the Microdroid boot sequence:

Secure bootflow of microdroid instance

Figure 1. Secure bootflow of microdroid instance

Here's an explanation of the steps:

  1. The bootloader is loaded into memory by crosvm and pvmfw starts executing. Before jumping to the bootloader, pvmfw performs two tasks:

    • Verifies the bootloader to check if it is from a trusted source (Google or an OEM).
    • Ensures that the same bootloader is used consistently across multiple boots of the same pVM through the use of the instance image. Specifically, the pVM is initially booted with an empty instance image. pvmfw stores the identity of the bootloader in the instance image and encrypts it. So, the next time the pVM is booted with the same instance image, pvmfw decrypts the saved identity from the instance image and verifies that it's the same that was previously saved. If the identities differ, pvmfw refuses to boot.

    The bootloader then boots Microdroid.

  2. The bootloader accesses the instance disk. Similar to pvmfw, the bootloader has an instance disk drive with information about partition images used in this instance during previous boots, including the public key.

  3. The bootloader verifies vbmeta and the chained partitions, such as boot and super, and, if successful, derives the next-stage pVM secrets. Then, Microdroid hands control over to the kernel.

  4. Because the super partition has already been verified by the bootloader (step 3), the kernel unconditionally mounts the super partition. As with the full Android, the super partition consists of multiple logical partitions mounted over dm-verity. Control is then passed to the init process, which starts various native services. The init.rc script is similar to that of full Android but tailored to the needs of Microdroid.

  5. The init process starts the Microdroid manager, which accesses the instance image. The Microdroid manager service decrypts the image using the key passed from the previous stage and reads the public keys and rollback counters of the client APK and APEXes that this pVM trusts. This information is used later by zipfuse and apexd when they mount the client APK and requested APEXes, respectively.

  6. The Microdroid manager service starts apexd.

  7. apexd mounts the APEXes at /apex/<name> directories. The only difference between how Android and Microdroid mounta APEXes is that in Microdroid, the APEX files are coming from virtual block devices (/dev/vdc1, …), not from regular files (/system/apex/*.apex).

  8. zipfuse is Microdroid’s FUSE file system. zipfuse mounts the client APK, which is essentially a Zip file as a file system. Underneath, the APK file is passed as a virtual block device by the pVM with dm-verity, same as APEX. The APK contains a config file with a list of APEXes that the app developer has requested for this pVM instance. The list is used by apexd when activating APEXes.

  9. The boot flow returns to the Microdroid manager service. The manager service then communicates with Android’s VirtualizationService using Binder RPC so that it can report important events like crash or shutdown, and accept requests such as terminating the pVM. The manager service reads the location of the main binary from the APK’s config file and executes it.

File exchange (AuthFS)

It's common for Android components to use files for input, output, and state and to pass these around as file descriptors (ParcelFileDescriptor type in AIDL) with access controlled by the Android kernel. AuthFS facilitates similar functionality for exchanging files between mutually distrusting endpoints across pVM boundaries.

Fundamentally, AuthFS is a remote file system with transparent integrity checks on individual access operations, similar to fs-verity. The checks allow the frontend, such as a file-reading program running in a pVM, to detect if the untrusted backend, typically Android, tampered with file content.

To exchange files, the backend (fd\_server) is started with per-file configuration specifying whether it's meant for input (read-only) or output (read-write). For input, the frontend enforces that the contents match a known hash, on top of a Merkle tree for on-access verification. For output, AuthFS internally maintains a hash tree of the contents as observed from write operations and can enforce integrity when the data are read back.

The underlying transport is currently based on Binder RPC, however that might change in the future to optimize performance.

Key management

pVMs are provided with a stable sealing key that's suitable for protected persistent data, and an attestation key that's suitable for producing signatures that are verifiably produced by the pVM.

Binder RPC

A majority of Android’s interfaces are expressed in AIDL, which is built on top of the Binder Linux kernel driver. To support interfaces between pVMs, the Binder protocol has been rewritten to work over sockets, vsock in the case of pVMs. Operating over sockets allows Android’s existing AIDL interfaces to be used in this new environment.

To set up the connection, one endpoint, such as pVM payload, creates an RpcServer object, registers a root object, and begins listening for new connections. Clients can connect to this server using an RpcSession object, get the Binder object, and use it exactly like a Binder object is used with the kernel Binder driver.