Virtual A/B Overview

Android has two update mechanisms: A/B (seamless) updates and non-A/B updates. To reduce code complexity and enhance the update process, in Android 11 the two mechanisms are unified through virtual A/B to bring seamless updates to all devices with a minimized cost of storage. Android 12 offers the option of Virtual A/B compression to compress snapshotted partitions. In both Android 11 and Android 12, the following apply:

  • Virtual A/B updates are seamless like A/B updates. Virtual A/B updates minimize the time that a device is offline and unusable.
  • Virtual A/B updates can be rolled back. If the new OS fails to boot, devices automatically roll back to the previous version.
  • Virtual A/B updates use a minimum of extra space by duplicating only the partitions that are used by the bootloader. Other updateable partitions are snapshotted.

Background and terminology

This section defines the terminology and describes the technology that supports virtual A/B.

Device-mapper

Device-mapper is a Linux virtual block layer used often in Android. With dynamic partitions, partitions like /system are a stack of layered devices:

  • At the bottom of the stack is the physical super partition (for example, /dev/block/by-name/super).
  • In the middle is a dm-linear device, specifying which blocks in the super partition form the given partition. This appears as /dev/block/mapper/system_[a|b] on an A/B device, or /dev/block/mapper/system on a non-A/B device.
  • At the top resides a dm-verity device, created for verified partitions. This device verifies that blocks on the dm-linear device are signed correctly. It appears as /dev/block/mapper/system-verity and is the source of the /system mount point.

Figure 1 shows what the stack under the /system mount point looks like.

Partition stacking underneath system

Figure 1. Stack under the /system mount point

dm-snapshot

Virtual A/B relies on dm-snapshot, a device-mapper module for snapshotting the state of a storage device. When using dm-snapshot, there are four devices in play:

  • The base device is the device that's snapshotted. On this page, the base device is always a dynamic partition, such as system or vendor.
  • The copy-on-write (COW) device, for logging changes to the base device. It can be any size, but it must be big enough to accommodate all changes to the base device.
  • The snapshot device is created using the snapshot target. Writes to the snapshot device are written to the COW device. Reads from the snapshot device read either from the base device or the COW device, depending on whether the data being accessed has been changed by the snapshot.
  • The origin device is created using the snapshot-origin target. Reads to the origin device read directly from the base device. Writes to the origin device write directly to the base device, but the original data is backed up by writing to the COW device.

Device mapping for dm-snapshot

Figure 2. Device mapping for dm-snapshot

Compressed snapshots

In Android 12, because space requirements on the /data partition can be high, you can enable compressed snapshots in your build to address the higher space requirements of the /data partition.

Virtual A/B compressed snapshots are built on top of two new components that are available in Android 12:

  • dm-user, a kernel module similar to FUSE that allows userspace to implement block devices.
  • snapuserd, a userspace daemon to implement a new snapshot format.

These components enable the compression. The other necessary changes made to implement the compressed snapshots capabilities are given in the next sections: COW format for compressed snapshots, dm-user, and Snapuserd.

COW format for compressed snapshots

In Android 12, compressed snapshots use a new COW format. Similar to the kernel's built-in format used for uncompressed snapshots, the COW format for the compressed snapshots has alternating sections of metadata and data. The original format's metadata only allowed for "replace" operations: Replace block X in the base image with the contents of block Y in the snapshot. The compressed snapshots COW format is more expressive and supports three operations:

  • Copy - Block X in the base device should be replaced with block Y in the base device.
  • Replace - Block X in the base device should be replaced with the contents of block Y in the snapshot. Each of these blocks is gz compressed.
  • Zero - Block X in the base device should be replaced with all zeroes.

Full OTA updates consist of replace and zero operations only. Incremental OTA updates can additionally have copy operations.

dm-user in Android 12

The dm-user kernel module enables userspace to implement device-mapper block devices. A dm-user table entry creates a miscellaneous device under /dev/dm-user/<control-name>. A userspace process can poll the device to receive read and write requests from the kernel. Each request has an associated buffer for userspace to either populate (for a read) or propagate (for a write).

The dm-user kernel module provides a new user-visible interface to the kernel that isn't part of the upstream kernel.org code base. Until it is, Google reserves the right to modify the dm-user interface in Android.

Snapuserd

The snapuserd userspace component to dm-user implements Virtual A/B Compression.

In the uncompressed version of Virtual A/B, (either in Android 11 and lower, or in Android 12 without the compressed snapshot option), the COW device is a raw file. When compression is enabled, the COW functions instead as a dm-user device, which is connected to an instance of the snapuserd daemon.

The kernel doesn’t use the new COW format. So the snapuserd component translates requests between the Android COW format and the kernel's built-in format:

Snapuserd component translating requests between Android COW format and kernel built-in format

Figure 3. Flow diagram of snapuserd as translator between Android and Kernel COW formats

This translation and decompression never occurs on disk. The snapuserd component intercepts the COW reads and writes that occur in the kernel, and implements them using the Android COW format.

Virtual A/B compression processes

These sections provide details regarding the processes used in Virtual A/B compression: reading metadata, merging, and conducting init transitions.

Reading metadata

Metadata is constructed by a snapuserd daemon. The metadata is primarily a mapping of 2 IDs, 8 bytes each, that represent the sectors to be merged. In dm-snapshot it’s called as disk_exception.

struct disk_exception {
    uint64_t old_chunk;
    uint64_t new_chunk;
};

A disk exception is used when an old chunk of data is replaced by a new one.

A Snapuserd daemon reads the internal COW file through the COW library and constructs the metadata for each of the COW operations present in the COW file.

Metadata reads are initiated from the dm-snapshot in the kernel when the dm- snapshot device is created.

The figure below provides a sequence diagram for the IO path for metadata construction.

Sequence diagram, IO path for metadata construction

Figure 4. Sequence flow for the IO path in metadata construction

Merging

Once the boot process is complete, the update engine marks the slot as boot successful and initiates the merge by switching the dm-snapshot target to the dm-snapshot-merge target.

dm-snapshot walks through the metadata and initiates a merge IO for each disk exception. A high-level overview of the merge IO path is shown below.

Merge IO path

Figure 5. Merge IO path overview

If the device is rebooted during the merge process, the merge resumes on the next reboot, and the merge is completed.

Init transitions

When booting with compressed snapshots, the first-stage init must start snapuserd to mount partitions. This poses a problem: When sepolicy is loaded and enforced, snapuserd gets put in the wrong context, and its read requests fail, with selinux denials.

To address this, snapuserd transitions in lock-step with init, as follows:

  1. First-stage init launches snapuserd from the ramdisk, and saves an open file-descriptor to it in an environment variable.
  2. First-stage init switches the root filesystem to the system partition, then executes the system copy of init.
  3. The system copy of init reads the combined sepolicy into a string.
  4. Init invokes mlock() on all ext4-backed pages. It then deactivates all device-mapper tables for snapshot devices, and stops snapuserd. After this it’s forbidden to read from partitions, since doing so causes deadlock.
  5. Using the open descriptor to the ramdisk copy of snapuserd, init relaunches the daemon with the correct selinux context. Device-mapper tables for snapshot devices are re-activated.
  6. Init invokes munlockall()- it’s safe to perform IO again.

Space usage

The following table provides a comparison of space usage for different OTA mechanisms using Pixel's OS and OTA sizes.

Size Impact non-A/B A/B Virtual A/B Virtual A/B (compressed)
Original Factory Image 4.5GB super (3.8G image + 700M reserved)1 9GB super (3.8G + 700M reserved, for two slots) 4.5GB super (3.8G image + 700M reserved) 4.5GB super (3.8G image + 700M reserved)
Other static Partitions /cache None None None
Additional storage During OTA (space returned after applying OTA) 1.4GB on /data 0 3.8GB2 on /data 2.1GB2 on /data
Total storage required to apply OTA 5.9GB3 (super and data) 9GB (super) 8.3GB3 (super and data) 6.6GB3 (super and data)

1Indicates assumed layout based on Pixel mapping.

2Assumes new system image is the same size as original.

3Space requirement is transient until reboot.

To implement Virtual A/B, or to use compressed snapshot capabilities, see Implementing Virtual A/B