Over-The-Air updates (OTAs) are a mechanism for operating system updates on Fuchsia. This document details how OTA updates work on Fuchsia.
The update process is divided into the following phases:
The two entry points for the operating system update process are the omaha-client
and the system-update-checker
components.
Both the omaha-client
and system-update-checker
serve the same purpose, to find out if there is an operating system update and start the update.
Note: Omaha is an update availability management protocol. For more information about Omaha, see Omaha.
Generally, products should use omaha-client
if they want to use Omaha to determine update availability. Products should use system-update-checker
if they don’t want to use Omaha and instead want to check for updates directly from package repositories.
On any given Fuchsia system, only one of these components may be running:
During the boot process, omaha-client
starts up and begins periodic update checks. During these checks, omaha-client
polls the Omaha server to check for updates.
The benefits of using Omaha are:
Note: Omaha is an update availability management protocol. For more information about Omaha, see Omaha.
Figure 1. A simplified version of the update check process with omaha-client
. There are policies that gate whether omaha-client
can check for an update or apply an update.
Once omaha-client
gets the update package URL from the Omaha server, omaha-client
tells the system-updater
to start an update.
Devices that don’t use omaha-client
use the system-update-checker
. Depending on how it is configured, the system-update-checker
regularly polls for an update package. These checks default to disabled if no auto_update
is specified.
To check if an update is available, the system-update-checker
checks the following conditions:
/pkgfs/system/meta
) different from the hash of system image (found in packages.json
) in the update package?If any of these answers are yes, then the system-update-checker
knows the update package has changed. Once the system-update-checker realizes the update package has changed, the system-update-checker
triggers the system-updater
to start an update using the default update package (fuchsia-pkg://fuchsia.com/update).
Figure 2. A simplified version of the update check process with the system-update-checker
.
Note: There is currently no way to check bootloader-only updates because there is no paver API to read the firmware. An update is not triggered even though the update package has changed. Until this is fixed, you should use update force-install <update-pkg-url>
to force an update.
If no update is required, the update checker saves the last known update package hash. On subsequent checks for an update, the hash of the update package that is fetched is checked against the last known hash. If the hashes are the same, no update is triggered. If the hashes are different, the vbmeta and ZBI are checked for changes to determine if an update is necessary.
Regardless if an update was triggered by omaha-client
or system-update-checker
, or even a forced update check, an update needs to be written to disk.
The update process is divided in the following steps:
Figure 3. The device is currently running hypothetical OS version 1 (on slot A) and begins to update to hypothetical OS version 2 (to slot B). Warning: this may not be how the disk is partitioned in practice.
Note: This does not garbage collect the old update package because the old update package is referenced in the dynamic index.
The system-updater
instructs pkg-cache
to perform garbage collection which deletes all BLOBs that aren’t referenced in either the static or dynamic indexes. This cleans up most of the BLOBs referenced by the old system.
Figure 4. The system-updater
instructs pkg-cache
to garbage collect all the blobs referenced by slot B. Since slot B currently references version 0, all of the version 0 blobs are garbage collected.
The system-updater
fetches the update package, using the provided update package URL. The dynamic index is then updated to reference the new update package. A sample update package may look like this:
/board /epoch.json /firmware /fuchsia.vbmeta /meta /packages.json /recovery.vbmeta /version /zbi.signed /zedboot.signed
Figure 5. The system-updater
instructs the pkg-resolver
to resolve the version 2 update package.
Optionally, update packages may contain an update-mode
file. This file determines whether the system update happens in Normal or ForceRecovery mode. If the update-mode file is not present, the system-updater
defaults to the Normal mode.
When the mode is ForceRecovery, the system-updater
writes an image to recovery, marks slots A and B as unbootable, then boots to recovery. For more information, see the implementation of ForceRecovery.
After the old update package is no longer referenced by the dynamic index, another garbage collection is triggered to delete the old update package. This step frees up additional space for any new packages.
Figure 6. The system-updater
instructs pkg-cache
to garbage collect the version 1 update package to free up space.
The current running system has a board file located in /config/build-info/board
. The system-updater
verifies that the board file on the system matches the board file in the update package.
Figure 7. The system-updater
verifies the board in the update package matches the board on slot A.
Update packages contain an epoch file (epoch.json
). If the epoch of the update package (the target epoch) is less than the epoch of the system-updater
(the source epoch), the OTA fails. For additional context, see RFC-0071.
Figure 8. The system-updater
verifies the epoch in the update package is supported by comparing it to the epoch of the current OS.
The system-updater parses the packages.json
file in the update package. The packages.json
looks like the following:
{ "version": “1”, "content": [ "fuchsia-pkg://fuchsia.com/sshd-host/0?hash=123..abc", "fuchsia-pkg://fuchsia.com/system-image/0?hash=456..def" ... ] }
The system-updater
instructs the pkg-resolver
to resolve all the package URLs. When fetching packages, the package management system only fetches BLOBs that are required for an update. This means that only BLOBs that don't exist or need to be updated on the system. The package management system fetches entire BLOBs, as opposed to a diff of whatever might currently be on the system.
Once all packages have been feteched, a BlobFS sync is triggered to flush the BLOBs to persistent storage. This process ensures that all the necessary BLOBs for the system update are available in BlobFS.
Figure 9. The system-updater
instructs the pkg-resolver to resolve the version 2 packages referenced in packages.json
.
The system-updater
determines which images need to be written to the block device. There are two kinds of images, assets and firmware.
Note: For more information on how this works, see the update.rs
file. To see the difference between assests and firmware images, see the paver.rs
file.
Then, the system-updater
instructs the paver to write the bootloader and firmware. The final location of these images does not depend on whether the device supports ABR. To prevent flash wear, the image is only written to a partition if the image is different from the image that already exists on the block device.
Note: To see more information on how the Fuchsia paver works for the bootloader, see fuchsia.paver
.
Then, the system-updater
instructs the paver to write the Fuchsia ZBI and its vbmeta. The final location of these images depends on whether the device supports ABR. If the device supports ABR, the paver writes the Fuchsia ZBI and its vbmeta to the slot that’s not currently booted (the alternate slot). Otherwise, the paver writes them to both the A and B partitions (if a B partition exists).
Note: To see more information on how the Fuchsia paver works for assets, see fuchsia.paver
.
Finally, the system-updater
instructs the paver to write the recovery ZBI and its vbmeta. Like the bootloader and firmware, the final location does not depend on if the device supports ABR.
Figure 10. The system-updater
writes the version 2 images to slot B via the paver.
If the device supports ABR, the system-updater
uses the paver to set the alternate partition as active. That way, the device boots into the alternate partition on the next boot.
There are a several ways to refer to the slot state. For example, the internal paver uses Successful
while the FIDL service uses Healthy
, while other cases may use Active, Inactive, Bootable, Unbootable, Current, Alternate, etc...
Note: For more information on how this is implemented, see data.h
.
The important metadata is 3 pieces of information that is stored for each kernel slot. This information helps determine the state of each kernel slot. For example, before slot B is marked as active, the metadata might look like:
Metadata | Slot A | Slot B |
---|---|---|
Priority | 15 | 0 |
Tries Remaining | 0 | 0 |
Healthy* | 1 | 0 |
After slot B is marked as active, the metadata would look like:
Metadata | Slot A | Slot B |
---|---|---|
Priority | 14 | 15** |
Tries Remaining | 0 | 7** |
Healthy | 1 | 0 |
Note: These numbers are based on hardcoded values for priority and remaining tries which are defined in data.h
.
If the device doesn’t support ABR, this check is skipped since there is no alternate partition. Instead, there is an active partition that is written to for every update.
Figure 11. The system-updater
sets slot B to Active, so that the device boots into slot B on the next boot.
Depending on the update configuration, the device may or may not reboot. After the device reboots, the device boots into the new slot.
Figure 12. The device reboots into slot B and begins running version 2.
The system commits an update once that update is verified by the system.
The system verifies the update in the following way:
Note: In this example, it is assumed that the update is written to partition B.
On the next boot, the bootloader needs to determine which slot to boot into. In this example, the bootloader determines to boot into slot B because slot B has a higher priority and more than 0 tries remaining (see Set alternate partition as active). Then, the bootloader verifies the ZBI of B matches the vbmeta of B, and finally boots into slot B.
Note: For more information on how the bootloader determines the slot to boot into, see flow.c
.
After early boot, fshost
launches pkgfs
using the new system-image package. This is the system image package that is referenced in the packages.json
while staging the update. The system-image package has a static_packages
file in it that lists the base packages for the new system. For example:
pkg-resolver/0 = new-version-hash-pkg-resolver foo/0 = new-version-hash-foo bar/0 = new-version-hash-bar ... // Note the system-image package is not referenced in static_packages // because it's impossible for it to refer to its own hash.
pkgfs
then loads all these packages as base packages. The packages appear in /pkgfs/{packages, versions}
, which indicate that the packages are installed or activated. Then, appmgr
starts which then starts the pkg-resolver
, pkg-cache
, netstack
, etc...
The system-update-committer
component runs various checks to verify if the new update was successful. For example, it instructs BlobFs to arbitrarily read 1MiB of data. If the system is already committed on boot, these checks are skipped. If the check fails and depending on how the system is configured, the system-update-committer
may trigger a reboot.
After the update is verified, the current partition (slot B) is marked as Healthy
. Using the example described in Set alternate partition as active, the boot metadata may now look like:
Metadata | Slot A | Slot B |
---|---|---|
Priority | 14 | 15 |
Tries Remaining | 7 | 0 |
Healthy | 0 | 1 |
Then, the alternate partition (slot A) is marked as unbootable. Now, the boot metadata may look like:
Metadata | Slot A | Slot B |
---|---|---|
Priority | 0 | 15 |
Tries Remaining | 0 | 0 |
Healthy | 0 | 1 |
After this, the update is considered committed. This means: