403 lines
11 KiB
C
403 lines
11 KiB
C
// SPDX-License-Identifier: BSD-3-Clause
|
|
/*
|
|
* Copyright 2019 The Fuchsia Authors. All rights reserved.
|
|
* Use of this source code is governed by a BSD-style license that can be
|
|
* found in the LICENSE file.
|
|
*
|
|
*/
|
|
|
|
#include "libabr.h"
|
|
#include <stdbool.h>
|
|
#include <stddef.h>
|
|
|
|
#define MIN_PAGE_SIZE 4096
|
|
#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))
|
|
|
|
static void abr_data_init(ABRData *data)
|
|
{
|
|
abr_memset(data, '\0', sizeof(ABRData));
|
|
abr_memcpy(data->magic, ABR_MAGIC, ABR_MAGIC_LEN);
|
|
data->version_major = ABR_MAJOR_VERSION;
|
|
data->version_minor = ABR_MINOR_VERSION;
|
|
data->slots[0].priority = ABR_MAX_PRIORITY;
|
|
data->slots[0].tries_remaining = ABR_MAX_TRIES_REMAINING;
|
|
data->slots[0].successful_boot = 0;
|
|
data->slots[1].priority = ABR_MAX_PRIORITY - 1;
|
|
data->slots[1].tries_remaining = ABR_MAX_TRIES_REMAINING;
|
|
data->slots[1].successful_boot = 0;
|
|
}
|
|
|
|
static bool slot_is_bootable(ABRSlotData *slot)
|
|
{
|
|
return (slot->priority > 0) &&
|
|
(slot->successful_boot || (slot->tries_remaining > 0));
|
|
}
|
|
|
|
static void slot_set_unbootable(ABRSlotData *slot)
|
|
{
|
|
slot->priority = 0;
|
|
slot->tries_remaining = 0;
|
|
slot->successful_boot = 0;
|
|
}
|
|
|
|
/* Ensure all unbootable and/or illegal states are marked as the
|
|
* canonical 'unbootable' state, e.g. priority=0, tries_remaining=0,
|
|
* and successful_boot=0.
|
|
*/
|
|
static void slot_normalize(ABRSlotData *slot)
|
|
{
|
|
if (slot->priority > 0) {
|
|
if (slot->tries_remaining == 0 && !slot->successful_boot) {
|
|
/* We've exhausted all tries -> unbootable. */
|
|
slot_set_unbootable(slot);
|
|
}
|
|
if (slot->tries_remaining > 0 && slot->successful_boot) {
|
|
/* Illegal state - abr_mark_slot_successful() will clear
|
|
* tries_remaining when setting successful_boot.
|
|
* Reset to default state.
|
|
*/
|
|
slot->tries_remaining = ABR_MAX_TRIES_REMAINING;
|
|
slot->successful_boot = 0;
|
|
/* having two slots with high priority will not cause an error */
|
|
slot->priority = ABR_MAX_PRIORITY;
|
|
}
|
|
} else {
|
|
slot_set_unbootable(slot);
|
|
}
|
|
}
|
|
|
|
static ABRResult save_metadata(ABROps *abr_ops, ABRData *data)
|
|
{
|
|
unsigned char serialized[MIN_PAGE_SIZE] = { 0 };
|
|
ABRResult ret;
|
|
size_t size = sizeof(ABRData);
|
|
|
|
abr_debug("Writing A/B metadata to disk.\n");
|
|
|
|
ret = abr_convert_data_to_buffer(data, serialized, size);
|
|
/* abr_convert_data_to_buffer fails only if buffer size is less
|
|
* than sizeof(ABRData). This situation should not happen here.
|
|
*/
|
|
abr_assert(ret == ABR_RESULT_OK);
|
|
|
|
abr_assert(abr_ops->write_abr_metadata_buffer);
|
|
|
|
ret =
|
|
abr_ops->write_abr_metadata_buffer(serialized, sizeof(serialized));
|
|
if (ret != ABR_RESULT_OK)
|
|
abr_error("Error writing A/B metadata.\n");
|
|
|
|
return ret;
|
|
}
|
|
|
|
/* Helper function to load A/B/R metadata - returns ABR_RESULT_OK on
|
|
* success, error code otherwise.
|
|
*/
|
|
static ABRResult load_metadata(ABROps *abr_ops,
|
|
ABRData *abr_data, ABRData *abr_data_orig)
|
|
{
|
|
unsigned char buf[MIN_PAGE_SIZE];
|
|
ABRResult ret;
|
|
size_t size = sizeof(buf);
|
|
|
|
abr_assert(abr_ops->read_abr_metadata_buffer);
|
|
|
|
ret = abr_ops->read_abr_metadata_buffer(buf, &size);
|
|
if (ret != ABR_RESULT_OK) {
|
|
abr_error("I/O error while loading A/B metadata buffer.\n");
|
|
return ret;
|
|
}
|
|
|
|
if (size != sizeof(buf)) {
|
|
abr_error("Wrong size of A/B/R metadata buffer.\n");
|
|
return ABR_RESULT_ERROR_IO;
|
|
}
|
|
|
|
if (abr_convert_buffer_to_data_and_verify(buf,
|
|
sizeof(ABRData),
|
|
abr_data) != ABR_RESULT_OK) {
|
|
abr_error("Error validating A/B metadata from disk.\n");
|
|
|
|
abr_data_init(abr_data);
|
|
|
|
abr_error("Resetting and writing new A/B metadata to disk.\n");
|
|
ret = save_metadata(abr_ops, abr_data);
|
|
if (ret != ABR_RESULT_OK)
|
|
return ret;
|
|
}
|
|
|
|
*abr_data_orig = *abr_data;
|
|
|
|
/* Ensure data is normalized, e.g. illegal states will be marked as
|
|
* unbootable and all unbootable states are represented with
|
|
* (priority=0, tries_remaining=0, successful_boot=0).
|
|
*/
|
|
slot_normalize(&abr_data->slots[0]);
|
|
slot_normalize(&abr_data->slots[1]);
|
|
return ABR_RESULT_OK;
|
|
}
|
|
|
|
/* Writes A/B/R metadata to disk only if it has changed - returns
|
|
* ABR_RESULT_OK on success, error code otherwise.
|
|
*/
|
|
static ABRResult save_metadata_if_changed(ABROps *abr_ops,
|
|
ABRData *abr_data,
|
|
ABRData *abr_data_orig)
|
|
{
|
|
if (abr_safe_memcmp(abr_data, abr_data_orig, sizeof(ABRData)) == 0)
|
|
return ABR_RESULT_OK;
|
|
|
|
return save_metadata(abr_ops, abr_data);
|
|
}
|
|
|
|
/* ==================== public API =================== */
|
|
|
|
const char *abr_get_slot_name_by_index(unsigned int index)
|
|
{
|
|
static const char * const slot_suffixes[] = { "_a", "_b", "_r" };
|
|
|
|
if (index >= ARRAY_SIZE(slot_suffixes)) {
|
|
abr_error("Invalid slot index\n");
|
|
return NULL;
|
|
}
|
|
|
|
return slot_suffixes[index];
|
|
}
|
|
|
|
unsigned int abr_get_boot_slot(ABROps *abr_ops, bool update_metadata,
|
|
bool *is_successful)
|
|
{
|
|
ABRData abr_data, abr_data_orig;
|
|
ABRResult ret;
|
|
unsigned int slot_idx_to_boot = ABR_R_SLOT_IDX;
|
|
|
|
if (is_successful)
|
|
*is_successful = false;
|
|
|
|
ret = load_metadata(abr_ops, &abr_data, &abr_data_orig);
|
|
if (ret != ABR_RESULT_OK) {
|
|
abr_error
|
|
("Failed to load metadata. Switching to 'recovery' mode.\n");
|
|
return ABR_R_SLOT_IDX;
|
|
}
|
|
// oneshot recovery boot has the highest priority
|
|
if (abr_data.oneshot_recovery_boot) {
|
|
abr_data.oneshot_recovery_boot = 0;
|
|
save_metadata(abr_ops, &abr_data);
|
|
return ABR_R_SLOT_IDX;
|
|
}
|
|
|
|
if (slot_is_bootable(&abr_data.slots[0])) {
|
|
if (is_successful)
|
|
*is_successful = true;
|
|
|
|
slot_idx_to_boot = 0;
|
|
if (slot_is_bootable(&abr_data.slots[1]) &&
|
|
abr_data.slots[1].priority > abr_data.slots[0].priority) {
|
|
slot_idx_to_boot = 1;
|
|
}
|
|
} else if (slot_is_bootable(&abr_data.slots[1])) {
|
|
if (is_successful)
|
|
*is_successful = true;
|
|
slot_idx_to_boot = 1;
|
|
}
|
|
|
|
if (update_metadata) {
|
|
/* ... and decrement tries remaining, if applicable.
|
|
* Note: no needs to check if tries_remainig > 0, because
|
|
* a slot will be marked as unbootable in `slot_normalize'
|
|
* if tries_remainig value is 0
|
|
*/
|
|
if (slot_idx_to_boot != ABR_R_SLOT_IDX &&
|
|
!abr_data.slots[slot_idx_to_boot].successful_boot) {
|
|
abr_data.slots[slot_idx_to_boot].tries_remaining--;
|
|
if (is_successful)
|
|
*is_successful = false;
|
|
save_metadata(abr_ops, &abr_data);
|
|
}
|
|
}
|
|
|
|
return slot_idx_to_boot;
|
|
}
|
|
|
|
ABRResult abr_mark_slot_active(ABROps *abr_ops, unsigned int slot_number)
|
|
{
|
|
ABRData abr_data, abr_data_orig;
|
|
unsigned int other_slot_number;
|
|
ABRResult ret;
|
|
|
|
if (slot_number > ABR_R_SLOT_IDX) {
|
|
abr_error("Wrong slot number.\n");
|
|
return ABR_RESULT_ERROR_INVALID_DATA;
|
|
}
|
|
|
|
if (slot_number == ABR_R_SLOT_IDX)
|
|
/* do nothing. R slot does not have metadata */
|
|
return ABR_RESULT_OK;
|
|
|
|
ret = load_metadata(abr_ops, &abr_data, &abr_data_orig);
|
|
if (ret != ABR_RESULT_OK)
|
|
return ret;
|
|
|
|
/* Make requested slot top priority, unsuccessful, and with max tries. */
|
|
abr_data.slots[slot_number].priority = ABR_MAX_PRIORITY;
|
|
abr_data.slots[slot_number].tries_remaining = ABR_MAX_TRIES_REMAINING;
|
|
abr_data.slots[slot_number].successful_boot = 0;
|
|
|
|
/* Ensure other slot doesn't have as high a priority. */
|
|
other_slot_number = 1 - slot_number;
|
|
if (abr_data.slots[other_slot_number].priority == ABR_MAX_PRIORITY) {
|
|
abr_data.slots[other_slot_number].priority =
|
|
ABR_MAX_PRIORITY - 1;
|
|
}
|
|
|
|
ret = save_metadata_if_changed(abr_ops, &abr_data, &abr_data_orig);
|
|
return ret;
|
|
}
|
|
|
|
ABRResult abr_mark_slot_unbootable(ABROps *abr_ops, unsigned int slot_number)
|
|
{
|
|
ABRData abr_data, abr_data_orig;
|
|
ABRResult ret;
|
|
|
|
if (slot_number > ABR_R_SLOT_IDX) {
|
|
abr_error("Wrong slot number.\n");
|
|
return ABR_RESULT_ERROR_INVALID_DATA;
|
|
}
|
|
|
|
if (slot_number == ABR_R_SLOT_IDX)
|
|
/* do nothing. R slot does not have metadata */
|
|
return ABR_RESULT_OK;
|
|
|
|
ret = load_metadata(abr_ops, &abr_data, &abr_data_orig);
|
|
if (ret != ABR_RESULT_OK)
|
|
return ret;
|
|
|
|
slot_set_unbootable(&abr_data.slots[slot_number]);
|
|
|
|
ret = save_metadata_if_changed(abr_ops, &abr_data, &abr_data_orig);
|
|
return ret;
|
|
}
|
|
|
|
ABRResult abr_mark_slot_successful(ABROps *abr_ops, unsigned int slot_number)
|
|
{
|
|
ABRData abr_data, abr_data_orig;
|
|
ABRResult ret;
|
|
|
|
if (slot_number > ABR_R_SLOT_IDX) {
|
|
abr_error("Wrong slot number.\n");
|
|
return ABR_RESULT_ERROR_INVALID_DATA;
|
|
}
|
|
|
|
if (slot_number == ABR_R_SLOT_IDX)
|
|
/* Do nothing. The R slot does not have metadata */
|
|
return ABR_RESULT_OK;
|
|
|
|
ret = load_metadata(abr_ops, &abr_data, &abr_data_orig);
|
|
if (ret != ABR_RESULT_OK)
|
|
return ret;
|
|
|
|
if (!slot_is_bootable(&abr_data.slots[slot_number])) {
|
|
abr_error("Cannot mark unbootable slot as successful.\n");
|
|
return ABR_RESULT_ERROR_INVALID_DATA;
|
|
}
|
|
|
|
abr_data.slots[slot_number].tries_remaining = 0;
|
|
abr_data.slots[slot_number].successful_boot = 1;
|
|
|
|
ret = save_metadata_if_changed(abr_ops, &abr_data, &abr_data_orig);
|
|
return ret;
|
|
}
|
|
|
|
ABRResult abr_get_slot_info(ABROps *abr_ops,
|
|
unsigned int slot_number, ABRSlotInfo *info)
|
|
{
|
|
ABRData abr_data, abr_data_orig;
|
|
ABRResult ret;
|
|
|
|
if (!info) {
|
|
abr_error("|info| is NULL.\n");
|
|
return ABR_RESULT_ERROR_INVALID_DATA;
|
|
}
|
|
|
|
abr_memset(info, 0, sizeof(ABRSlotInfo));
|
|
|
|
if (slot_number > ABR_R_SLOT_IDX) {
|
|
abr_error("Wrong slot number.\n");
|
|
return ABR_RESULT_ERROR_INVALID_DATA;
|
|
}
|
|
|
|
if (slot_number == ABR_R_SLOT_IDX) {
|
|
/* assume that R slot is always OK */
|
|
info->valid = true;
|
|
info->successful_boot = true;
|
|
info->retry_count = 0;
|
|
return ABR_RESULT_OK;
|
|
}
|
|
|
|
ret = load_metadata(abr_ops, &abr_data, &abr_data_orig);
|
|
if (ret != ABR_RESULT_OK)
|
|
return ret;
|
|
|
|
info->valid = slot_is_bootable(&abr_data.slots[slot_number]);
|
|
info->successful_boot = abr_data.slots[slot_number].successful_boot;
|
|
info->retry_count = abr_data.slots[slot_number].tries_remaining;
|
|
|
|
return ABR_RESULT_OK;
|
|
}
|
|
|
|
ABRResult abr_convert_buffer_to_data_and_verify(const unsigned char *src,
|
|
size_t size, ABRData *dest)
|
|
{
|
|
ABRData *data = (ABRData *)src;
|
|
|
|
if (size != sizeof(ABRData)) {
|
|
abr_error("Wrong buffer size.\n");
|
|
return ABR_RESULT_ERROR_INVALID_DATA;
|
|
}
|
|
|
|
/* Ensure magic is correct. */
|
|
if (abr_safe_memcmp(data->magic, ABR_MAGIC, ABR_MAGIC_LEN) != 0) {
|
|
abr_error("Magic is incorrect.\n");
|
|
return ABR_RESULT_ERROR_INVALID_DATA;
|
|
}
|
|
|
|
abr_memcpy(dest, data, sizeof(ABRData));
|
|
dest->crc32 = abr_be32toh(dest->crc32);
|
|
|
|
/* Ensure we don't attempt to access any fields if the major version
|
|
* is not supported.
|
|
*/
|
|
if (dest->version_major > ABR_MAJOR_VERSION) {
|
|
abr_error("No support for given major version.\n");
|
|
return ABR_RESULT_ERROR_UNSUPPORTED_VERSION;
|
|
}
|
|
|
|
/* Bail if CRC32 doesn't match. */
|
|
if (dest->crc32 !=
|
|
abr_crc32((const uint8_t *)dest,
|
|
sizeof(ABRData) - sizeof(uint32_t))) {
|
|
abr_error("CRC32 does not match.\n");
|
|
return ABR_RESULT_ERROR_INVALID_DATA;
|
|
}
|
|
|
|
return ABR_RESULT_OK;
|
|
}
|
|
|
|
ABRResult abr_convert_data_to_buffer(const ABRData *src,
|
|
unsigned char *dest, size_t size)
|
|
{
|
|
ABRData *dest_data = (ABRData *)dest;
|
|
|
|
if (size != sizeof(ABRData)) {
|
|
abr_error("Wrong buffer size.\n");
|
|
return ABR_RESULT_ERROR_INVALID_DATA;
|
|
}
|
|
|
|
abr_memcpy(dest, src, sizeof(ABRData));
|
|
dest_data->crc32 =
|
|
abr_htobe32(abr_crc32(dest, sizeof(ABRData) - sizeof(uint32_t)));
|
|
|
|
return ABR_RESULT_OK;
|
|
}
|