| // 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; |
| } |