1106 lines
32 KiB
C
1106 lines
32 KiB
C
/*
|
|
* Copyright 2018 NXP
|
|
*/
|
|
|
|
#include <common.h>
|
|
#include <fsl_avb.h>
|
|
#include <mmc.h>
|
|
#include <spl.h>
|
|
#include <part.h>
|
|
#include <image.h>
|
|
#include "utils.h"
|
|
#include "fsl_caam.h"
|
|
#include "fsl_avbkey.h"
|
|
|
|
#if defined(CONFIG_DUAL_BOOTLOADER) || !defined(CONFIG_SPL_BUILD)
|
|
static const char* slot_suffixes[2] = {"_a", "_b"};
|
|
|
|
/* This is a copy of slot_set_unbootable() form
|
|
* external/avb/libavb_ab/avb_ab_flow.c.
|
|
*/
|
|
void fsl_slot_set_unbootable(AvbABSlotData* 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. This is a copy of slot_normalize from
|
|
* external/avb/libavb_ab/avb_ab_flow.c.
|
|
*/
|
|
void fsl_slot_normalize(AvbABSlotData* slot) {
|
|
if (slot->priority > 0) {
|
|
#if defined(CONFIG_DUAL_BOOTLOADER) && !defined(CONFIG_SPL_BUILD)
|
|
if ((slot->tries_remaining == 0)
|
|
&& (!slot->successful_boot) && (slot->bootloader_verified != 1)) {
|
|
/* We've exhausted all tries -> unbootable. */
|
|
fsl_slot_set_unbootable(slot);
|
|
}
|
|
#else
|
|
if ((slot->tries_remaining == 0) && (!slot->successful_boot)) {
|
|
/* We've exhausted all tries -> unbootable. */
|
|
fsl_slot_set_unbootable(slot);
|
|
}
|
|
#endif
|
|
if ((slot->tries_remaining > 0) && (slot->successful_boot)) {
|
|
/* Illegal state - avb_ab_mark_slot_successful() will clear
|
|
* tries_remaining when setting successful_boot.
|
|
*/
|
|
fsl_slot_set_unbootable(slot);
|
|
}
|
|
} else {
|
|
fsl_slot_set_unbootable(slot);
|
|
}
|
|
}
|
|
|
|
/* This is a copy of slot_is_bootable() from
|
|
* externel/avb/libavb_ab/avb_ab_flow.c.
|
|
*/
|
|
bool fsl_slot_is_bootable(AvbABSlotData* slot) {
|
|
return (slot->priority > 0) &&
|
|
(slot->successful_boot || (slot->tries_remaining > 0));
|
|
}
|
|
#endif /* CONFIG_DUAL_BOOTLOADER || !CONFIG_SPL_BUILD */
|
|
|
|
#if defined(CONFIG_DUAL_BOOTLOADER) && defined(CONFIG_SPL_BUILD)
|
|
|
|
#define FSL_AB_METADATA_MISC_PARTITION_OFFSET 2048
|
|
#define PARTITION_NAME_LEN 13
|
|
#define PARTITION_MISC "misc"
|
|
#define PARTITION_BOOTLOADER "bootloader"
|
|
|
|
extern int mmc_switch(struct mmc *mmc, u8 set, u8 index, u8 value);
|
|
extern int mmc_load_image_parse_container(struct spl_image_info *spl_image,
|
|
struct mmc *mmc, unsigned long sector);
|
|
|
|
/* Pre-declaration of h_spl_load_read(), see detail implementation in
|
|
* common/spl/spl_mmc.c.
|
|
*/
|
|
ulong h_spl_load_read(struct spl_load_info *load, ulong sector,
|
|
ulong count, void *buf);
|
|
|
|
void fsl_avb_ab_data_update_crc_and_byteswap(const AvbABData* src,
|
|
AvbABData* dest) {
|
|
memcpy(dest, src, sizeof(AvbABData));
|
|
dest->crc32 = cpu_to_be32(
|
|
avb_crc32((const uint8_t*)dest,
|
|
sizeof(AvbABData) - sizeof(uint32_t)));
|
|
}
|
|
|
|
void fsl_avb_ab_data_init(AvbABData* data) {
|
|
memset(data, '\0', sizeof(AvbABData));
|
|
memcpy(data->magic, AVB_AB_MAGIC, AVB_AB_MAGIC_LEN);
|
|
data->version_major = AVB_AB_MAJOR_VERSION;
|
|
data->version_minor = AVB_AB_MINOR_VERSION;
|
|
data->slots[0].priority = AVB_AB_MAX_PRIORITY;
|
|
data->slots[0].tries_remaining = AVB_AB_MAX_TRIES_REMAINING;
|
|
data->slots[0].successful_boot = 0;
|
|
data->slots[0].bootloader_verified = 0;
|
|
data->slots[1].priority = AVB_AB_MAX_PRIORITY - 1;
|
|
data->slots[1].tries_remaining = AVB_AB_MAX_TRIES_REMAINING;
|
|
data->slots[1].successful_boot = 0;
|
|
data->slots[1].bootloader_verified = 0;
|
|
}
|
|
|
|
bool fsl_avb_ab_data_verify_and_byteswap(const AvbABData* src,
|
|
AvbABData* dest) {
|
|
/* Ensure magic is correct. */
|
|
if (memcmp(src->magic, AVB_AB_MAGIC, AVB_AB_MAGIC_LEN) != 0) {
|
|
printf("Magic is incorrect.\n");
|
|
return false;
|
|
}
|
|
|
|
memcpy(dest, src, sizeof(AvbABData));
|
|
dest->crc32 = be32_to_cpu(dest->crc32);
|
|
|
|
/* Ensure we don't attempt to access any fields if the major version
|
|
* is not supported.
|
|
*/
|
|
if (dest->version_major > AVB_AB_MAJOR_VERSION) {
|
|
printf("No support for given major version.\n");
|
|
return false;
|
|
}
|
|
|
|
/* Fail if CRC32 doesn't match. */
|
|
if (dest->crc32 !=
|
|
avb_crc32((const uint8_t*)dest, sizeof(AvbABData) - sizeof(uint32_t))) {
|
|
printf("CRC32 does not match.\n");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/* Writes A/B metadata to disk only if it has changed.
|
|
*/
|
|
int fsl_save_metadata_if_changed_dual_uboot(struct blk_desc *dev_desc,
|
|
AvbABData* ab_data,
|
|
AvbABData* ab_data_orig) {
|
|
AvbABData serialized;
|
|
size_t num_bytes;
|
|
disk_partition_t info;
|
|
|
|
/* Save metadata if changed. */
|
|
if (memcmp(ab_data, ab_data_orig, sizeof(AvbABData)) != 0) {
|
|
/* Get misc partition info */
|
|
if (part_get_info_by_name(dev_desc, PARTITION_MISC, &info) == -1) {
|
|
printf("Can't get partition info of partition: misc\n");
|
|
return -1;
|
|
}
|
|
|
|
/* Writing A/B metadata to disk. */
|
|
fsl_avb_ab_data_update_crc_and_byteswap(ab_data, &serialized);
|
|
if (write_to_partition_in_bytes(dev_desc, &info,
|
|
FSL_AB_METADATA_MISC_PARTITION_OFFSET,
|
|
sizeof(AvbABData),
|
|
(void *)&serialized, &num_bytes) ||
|
|
(num_bytes != sizeof(AvbABData))) {
|
|
printf("Error--write metadata fail!\n");
|
|
return -1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Load metadate from misc partition.
|
|
*/
|
|
int fsl_load_metadata_dual_uboot(struct blk_desc *dev_desc,
|
|
AvbABData* ab_data,
|
|
AvbABData* ab_data_orig) {
|
|
disk_partition_t info;
|
|
AvbABData serialized;
|
|
size_t num_bytes;
|
|
|
|
if (part_get_info_by_name(dev_desc, PARTITION_MISC, &info) == -1) {
|
|
printf("Can't get partition info of partition: misc\n");
|
|
return -1;
|
|
} else {
|
|
read_from_partition_in_bytes(dev_desc, &info,
|
|
FSL_AB_METADATA_MISC_PARTITION_OFFSET,
|
|
sizeof(AvbABData),
|
|
(void *)ab_data, &num_bytes );
|
|
if (num_bytes != sizeof(AvbABData)) {
|
|
printf("Error--read metadata fail!\n");
|
|
return -1;
|
|
} else {
|
|
if (!fsl_avb_ab_data_verify_and_byteswap(ab_data, &serialized)) {
|
|
printf("Error validating A/B metadata from disk.\n");
|
|
printf("Resetting and writing new A/B metadata to disk.\n");
|
|
fsl_avb_ab_data_init(ab_data);
|
|
fsl_avb_ab_data_update_crc_and_byteswap(ab_data, &serialized);
|
|
num_bytes = 0;
|
|
if (write_to_partition_in_bytes(
|
|
dev_desc, &info,
|
|
FSL_AB_METADATA_MISC_PARTITION_OFFSET,
|
|
sizeof(AvbABData),
|
|
(void *)&serialized, &num_bytes) ||
|
|
(num_bytes != sizeof(AvbABData))) {
|
|
printf("Error--write metadata fail!\n");
|
|
return -1;
|
|
} else
|
|
return 0;
|
|
} else {
|
|
memcpy(ab_data_orig, ab_data, sizeof(AvbABData));
|
|
/* 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).
|
|
*/
|
|
fsl_slot_normalize(&ab_data->slots[0]);
|
|
fsl_slot_normalize(&ab_data->slots[1]);
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifndef CONFIG_XEN
|
|
static int spl_verify_rbidx(struct mmc *mmc, AvbABSlotData *slot,
|
|
struct spl_image_info *spl_image)
|
|
{
|
|
kblb_hdr_t hdr;
|
|
kblb_tag_t *rbk;
|
|
uint64_t extract_idx;
|
|
#ifdef CONFIG_AVB_ATX
|
|
struct bl_rbindex_package *bl_rbindex;
|
|
#endif
|
|
|
|
/* Make sure rollback index has been initialized before verify */
|
|
if (rpmb_init()) {
|
|
printf("RPMB init failed!\n");
|
|
return -1;
|
|
}
|
|
|
|
/* Read bootloader rollback index header first. */
|
|
if (rpmb_read(mmc, (uint8_t *)&hdr, sizeof(hdr),
|
|
BOOTLOADER_RBIDX_OFFSET) != 0) {
|
|
printf("Read RPMB error!\n");
|
|
return -1;
|
|
}
|
|
|
|
/* Read bootloader rollback index. */
|
|
rbk = &(hdr.bootloader_rbk_tags);
|
|
if (rpmb_read(mmc, (uint8_t *)&extract_idx, rbk->len, rbk->offset) != 0) {
|
|
printf("Read rollback index error!\n");
|
|
return -1;
|
|
}
|
|
|
|
/* Verify bootloader rollback index. */
|
|
if (spl_image->rbindex >= extract_idx) {
|
|
/* Rollback index verify pass, update it only when current slot
|
|
* has been marked as successful.
|
|
*/
|
|
if ((slot->successful_boot != 0) && (spl_image->rbindex != extract_idx) &&
|
|
rpmb_write(mmc, (uint8_t *)(&(spl_image->rbindex)),
|
|
rbk->len, rbk->offset)) {
|
|
printf("Update bootloader rollback index failed!\n");
|
|
return -1;
|
|
}
|
|
|
|
#ifdef CONFIG_AVB_ATX
|
|
/* Pass bootloader rbindex to u-boot here. */
|
|
bl_rbindex = (struct bl_rbindex_package *)BL_RBINDEX_LOAD_ADDR;
|
|
memcpy(bl_rbindex->magic, BL_RBINDEX_MAGIC, BL_RBINDEX_MAGIC_LEN);
|
|
if (slot->successful_boot != 0)
|
|
bl_rbindex->rbindex = spl_image->rbindex;
|
|
else
|
|
bl_rbindex->rbindex = extract_idx;
|
|
#endif
|
|
|
|
return 0;
|
|
} else {
|
|
printf("Rollback index verify rejected!\n");
|
|
return -1;
|
|
}
|
|
|
|
}
|
|
#endif /* CONFIG_XEN */
|
|
|
|
#ifdef CONFIG_PARSE_CONTAINER
|
|
int mmc_load_image_parse_container_dual_uboot(
|
|
struct spl_image_info *spl_image, struct mmc *mmc)
|
|
{
|
|
disk_partition_t info;
|
|
int ret = 0, n = 0;
|
|
char partition_name[PARTITION_NAME_LEN];
|
|
struct blk_desc *dev_desc;
|
|
AvbABData ab_data, ab_data_orig;
|
|
size_t slot_index_to_boot, target_slot;
|
|
#ifndef CONFIG_XEN
|
|
struct keyslot_package kp;
|
|
#endif
|
|
|
|
/* Check if gpt is valid */
|
|
dev_desc = mmc_get_blk_desc(mmc);
|
|
if (dev_desc) {
|
|
if (part_get_info(dev_desc, 1, &info)) {
|
|
printf("GPT is invalid, please flash correct GPT!\n");
|
|
return -1;
|
|
}
|
|
} else {
|
|
printf("Get block desc fail!\n");
|
|
return -1;
|
|
}
|
|
|
|
#ifndef CONFIG_XEN
|
|
/* Read RPMB keyslot package, xen won't check this. */
|
|
read_keyslot_package(&kp);
|
|
if (strcmp(kp.magic, KEYPACK_MAGIC)) {
|
|
if (rpmbkey_is_set()) {
|
|
printf("\nFATAL - RPMB key was destroyed!\n");
|
|
hang();
|
|
} else
|
|
printf("keyslot package magic error, do nothing here!\n");
|
|
} else {
|
|
/* Set power-on write protection to boot1 partition. */
|
|
if (mmc_switch(mmc, EXT_CSD_CMD_SET_NORMAL, EXT_CSD_BOOT_WP, BOOT1_PWR_WP)) {
|
|
printf("Unable to set power-on write protection to boot1!\n");
|
|
return -1;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/* Load AB metadata from misc partition */
|
|
if (fsl_load_metadata_dual_uboot(dev_desc, &ab_data,
|
|
&ab_data_orig)) {
|
|
return -1;
|
|
}
|
|
|
|
slot_index_to_boot = 2; // Means not 0 or 1
|
|
target_slot =
|
|
(ab_data.slots[1].priority > ab_data.slots[0].priority) ? 1 : 0;
|
|
|
|
for (n = 0; n < 2; n++) {
|
|
if (!fsl_slot_is_bootable(&ab_data.slots[target_slot])) {
|
|
target_slot = (target_slot == 1 ? 0 : 1);
|
|
continue;
|
|
}
|
|
/* Choose slot to load. */
|
|
snprintf(partition_name, PARTITION_NAME_LEN,
|
|
PARTITION_BOOTLOADER"%s",
|
|
slot_suffixes[target_slot]);
|
|
|
|
/* Read part info from gpt */
|
|
if (part_get_info_by_name(dev_desc, partition_name, &info) == -1) {
|
|
printf("Can't get partition info of partition bootloader%s\n",
|
|
slot_suffixes[target_slot]);
|
|
ret = -1;
|
|
goto end;
|
|
} else {
|
|
ret = mmc_load_image_parse_container(spl_image, mmc, info.start);
|
|
|
|
/* Don't need to check rollback index for xen. */
|
|
#ifndef CONFIG_XEN
|
|
/* Image loaded successfully, go to verify rollback index */
|
|
if (!ret && rpmbkey_is_set())
|
|
ret = spl_verify_rbidx(mmc, &ab_data.slots[target_slot], spl_image);
|
|
|
|
/* Copy rpmb keyslot to secure memory. */
|
|
if (!ret)
|
|
fill_secure_keyslot_package(&kp);
|
|
#endif
|
|
}
|
|
|
|
/* Set current slot to unbootable if load/verify fail. */
|
|
if (ret != 0) {
|
|
printf("Load or verify bootloader%s fail, setting unbootable..\n",
|
|
slot_suffixes[target_slot]);
|
|
fsl_slot_set_unbootable(&ab_data.slots[target_slot]);
|
|
/* Switch to another slot. */
|
|
target_slot = (target_slot == 1 ? 0 : 1);
|
|
} else {
|
|
slot_index_to_boot = target_slot;
|
|
n = 2;
|
|
}
|
|
}
|
|
|
|
if (slot_index_to_boot == 2) {
|
|
/* No bootable slots! */
|
|
printf("No bootable slots found.\n");
|
|
ret = -1;
|
|
goto end;
|
|
} else if (!ab_data.slots[slot_index_to_boot].successful_boot &&
|
|
(ab_data.slots[slot_index_to_boot].tries_remaining > 0)) {
|
|
/* Set the bootloader_verified flag if current slot only has one chance. */
|
|
if (ab_data.slots[slot_index_to_boot].tries_remaining == 1)
|
|
ab_data.slots[slot_index_to_boot].bootloader_verified = 1;
|
|
ab_data.slots[slot_index_to_boot].tries_remaining -= 1;
|
|
}
|
|
printf("Booting from bootloader%s...\n", slot_suffixes[slot_index_to_boot]);
|
|
|
|
end:
|
|
/* Save metadata if changed. */
|
|
if (fsl_save_metadata_if_changed_dual_uboot(dev_desc, &ab_data, &ab_data_orig)) {
|
|
ret = -1;
|
|
}
|
|
|
|
if (ret)
|
|
return -1;
|
|
else
|
|
return 0;
|
|
}
|
|
#else /* CONFIG_PARSE_CONTAINER */
|
|
int mmc_load_image_raw_sector_dual_uboot(
|
|
struct spl_image_info *spl_image, struct mmc *mmc)
|
|
{
|
|
unsigned long count;
|
|
disk_partition_t info;
|
|
int ret = 0, n = 0;
|
|
char partition_name[PARTITION_NAME_LEN];
|
|
struct blk_desc *dev_desc;
|
|
struct image_header *header;
|
|
AvbABData ab_data, ab_data_orig;
|
|
size_t slot_index_to_boot, target_slot;
|
|
struct keyslot_package kp;
|
|
|
|
/* Check if gpt is valid */
|
|
dev_desc = mmc_get_blk_desc(mmc);
|
|
if (dev_desc) {
|
|
if (part_get_info(dev_desc, 1, &info)) {
|
|
printf("GPT is invalid, please flash correct GPT!\n");
|
|
return -1;
|
|
}
|
|
} else {
|
|
printf("Get block desc fail!\n");
|
|
return -1;
|
|
}
|
|
|
|
/* Init RPMB keyslot package if not initialized before. */
|
|
read_keyslot_package(&kp);
|
|
if (strcmp(kp.magic, KEYPACK_MAGIC)) {
|
|
printf("keyslot package magic error. Will generate new one\n");
|
|
if (gen_rpmb_key(&kp)) {
|
|
printf("Generate keyslot package fail!\n");
|
|
return -1;
|
|
}
|
|
}
|
|
/* Set power-on write protection to boot1 partition. */
|
|
if (mmc_switch(mmc, EXT_CSD_CMD_SET_NORMAL, EXT_CSD_BOOT_WP, BOOT1_PWR_WP)) {
|
|
printf("Unable to set power-on write protection to boot1!\n");
|
|
return -1;
|
|
}
|
|
|
|
/* Load AB metadata from misc partition */
|
|
if (fsl_load_metadata_dual_uboot(dev_desc, &ab_data,
|
|
&ab_data_orig)) {
|
|
return -1;
|
|
}
|
|
|
|
slot_index_to_boot = 2; // Means not 0 or 1
|
|
target_slot =
|
|
(ab_data.slots[1].priority > ab_data.slots[0].priority) ? 1 : 0;
|
|
|
|
for (n = 0; n < 2; n++) {
|
|
if (!fsl_slot_is_bootable(&ab_data.slots[target_slot])) {
|
|
target_slot = (target_slot == 1 ? 0 : 1);
|
|
continue;
|
|
}
|
|
/* Choose slot to load. */
|
|
snprintf(partition_name, PARTITION_NAME_LEN,
|
|
PARTITION_BOOTLOADER"%s",
|
|
slot_suffixes[target_slot]);
|
|
|
|
/* Read part info from gpt */
|
|
if (part_get_info_by_name(dev_desc, partition_name, &info) == -1) {
|
|
printf("Can't get partition info of partition bootloader%s\n",
|
|
slot_suffixes[target_slot]);
|
|
ret = -1;
|
|
goto end;
|
|
} else {
|
|
header = (struct image_header *)(CONFIG_SYS_TEXT_BASE -
|
|
sizeof(struct image_header));
|
|
|
|
/* read image header to find the image size & load address */
|
|
count = blk_dread(dev_desc, info.start, 1, header);
|
|
if (count == 0) {
|
|
ret = -1;
|
|
goto end;
|
|
}
|
|
|
|
/* Load fit and check HAB */
|
|
if (IS_ENABLED(CONFIG_SPL_LOAD_FIT) &&
|
|
image_get_magic(header) == FDT_MAGIC) {
|
|
struct spl_load_info load;
|
|
|
|
debug("Found FIT\n");
|
|
load.dev = mmc;
|
|
load.priv = NULL;
|
|
load.filename = NULL;
|
|
load.bl_len = mmc->read_bl_len;
|
|
load.read = h_spl_load_read;
|
|
ret = spl_load_simple_fit(spl_image, &load,
|
|
info.start, header);
|
|
} else {
|
|
ret = -1;
|
|
}
|
|
|
|
/* Fit image loaded successfully, go to verify rollback index */
|
|
if (!ret)
|
|
ret = spl_verify_rbidx(mmc, &ab_data.slots[target_slot], spl_image);
|
|
|
|
/* Copy rpmb keyslot to secure memory. */
|
|
if (!ret)
|
|
fill_secure_keyslot_package(&kp);
|
|
}
|
|
|
|
/* Set current slot to unbootable if load/verify fail. */
|
|
if (ret != 0) {
|
|
printf("Load or verify bootloader%s fail, setting unbootable..\n",
|
|
slot_suffixes[target_slot]);
|
|
fsl_slot_set_unbootable(&ab_data.slots[target_slot]);
|
|
/* Switch to another slot. */
|
|
target_slot = (target_slot == 1 ? 0 : 1);
|
|
} else {
|
|
slot_index_to_boot = target_slot;
|
|
n = 2;
|
|
}
|
|
}
|
|
|
|
if (slot_index_to_boot == 2) {
|
|
/* No bootable slots! */
|
|
printf("No bootable slots found.\n");
|
|
ret = -1;
|
|
goto end;
|
|
} else if (!ab_data.slots[slot_index_to_boot].successful_boot &&
|
|
(ab_data.slots[slot_index_to_boot].tries_remaining > 0)) {
|
|
/* Set the bootloader_verified flag as if current slot only has one chance. */
|
|
if (ab_data.slots[slot_index_to_boot].tries_remaining == 1)
|
|
ab_data.slots[slot_index_to_boot].bootloader_verified = 1;
|
|
ab_data.slots[slot_index_to_boot].tries_remaining -= 1;
|
|
}
|
|
printf("Booting from bootloader%s...\n", slot_suffixes[slot_index_to_boot]);
|
|
|
|
end:
|
|
/* Save metadata if changed. */
|
|
if (fsl_save_metadata_if_changed_dual_uboot(dev_desc, &ab_data, &ab_data_orig)) {
|
|
ret = -1;
|
|
}
|
|
|
|
if (ret)
|
|
return -1;
|
|
else
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* spl_fit_get_rbindex(): Get rollback index of the bootloader.
|
|
* @fit: Pointer to the FDT blob.
|
|
* @images: Offset of the /images subnode.
|
|
*
|
|
* Return: the rollback index value of bootloader or a negative
|
|
* error number.
|
|
*/
|
|
int spl_fit_get_rbindex(const void *fit, int images)
|
|
{
|
|
const char *str;
|
|
uint64_t index;
|
|
int conf_node;
|
|
int len;
|
|
|
|
conf_node = fit_find_config_node(fit);
|
|
if (conf_node < 0) {
|
|
return conf_node;
|
|
}
|
|
|
|
str = fdt_getprop(fit, conf_node, "rbindex", &len);
|
|
if (!str) {
|
|
debug("cannot find property 'rbindex'\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
index = simple_strtoul(str, NULL, 10);
|
|
|
|
return index;
|
|
}
|
|
#endif /* CONFIG_PARSE_CONTAINER */
|
|
|
|
/* For normal build */
|
|
#elif !defined(CONFIG_SPL_BUILD)
|
|
|
|
/* Writes A/B metadata to disk only if it has been changed.
|
|
*/
|
|
static AvbIOResult fsl_save_metadata_if_changed(AvbABOps* ab_ops,
|
|
AvbABData* ab_data,
|
|
AvbABData* ab_data_orig) {
|
|
if (avb_safe_memcmp(ab_data, ab_data_orig, sizeof(AvbABData)) != 0) {
|
|
avb_debug("Writing A/B metadata to disk.\n");
|
|
return ab_ops->write_ab_metadata(ab_ops, ab_data);
|
|
}
|
|
return AVB_IO_RESULT_OK;
|
|
}
|
|
|
|
/* Helper function to load metadata - returns AVB_IO_RESULT_OK on
|
|
* success, error code otherwise. This is a copy of load_metadata()
|
|
* from /lib/avb/libavb_ab/avb_ab_flow.c.
|
|
*/
|
|
static AvbIOResult fsl_load_metadata(AvbABOps* ab_ops,
|
|
AvbABData* ab_data,
|
|
AvbABData* ab_data_orig) {
|
|
AvbIOResult io_ret;
|
|
|
|
io_ret = ab_ops->read_ab_metadata(ab_ops, ab_data);
|
|
if (io_ret != AVB_IO_RESULT_OK) {
|
|
avb_error("I/O error while loading A/B metadata.\n");
|
|
return io_ret;
|
|
}
|
|
*ab_data_orig = *ab_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).
|
|
*/
|
|
fsl_slot_normalize(&ab_data->slots[0]);
|
|
fsl_slot_normalize(&ab_data->slots[1]);
|
|
return AVB_IO_RESULT_OK;
|
|
}
|
|
|
|
#ifdef CONFIG_DUAL_BOOTLOADER
|
|
AvbABFlowResult avb_flow_dual_uboot(AvbABOps* ab_ops,
|
|
const char* const* requested_partitions,
|
|
AvbSlotVerifyFlags flags,
|
|
AvbHashtreeErrorMode hashtree_error_mode,
|
|
AvbSlotVerifyData** out_data) {
|
|
AvbOps* ops = ab_ops->ops;
|
|
AvbSlotVerifyData* slot_data = NULL;
|
|
AvbSlotVerifyData* data = NULL;
|
|
AvbABFlowResult ret;
|
|
AvbABData ab_data, ab_data_orig;
|
|
AvbIOResult io_ret;
|
|
bool saw_and_allowed_verification_error = false;
|
|
AvbSlotVerifyResult verify_result;
|
|
bool set_slot_unbootable = false;
|
|
int target_slot, n;
|
|
uint64_t rollback_index_value = 0;
|
|
uint64_t current_rollback_index_value = 0;
|
|
|
|
io_ret = fsl_load_metadata(ab_ops, &ab_data, &ab_data_orig);
|
|
if (io_ret == AVB_IO_RESULT_ERROR_OOM) {
|
|
ret = AVB_AB_FLOW_RESULT_ERROR_OOM;
|
|
goto out;
|
|
} else if (io_ret != AVB_IO_RESULT_OK) {
|
|
ret = AVB_AB_FLOW_RESULT_ERROR_IO;
|
|
goto out;
|
|
}
|
|
|
|
/* Choose the target slot, it should be the same with the one in SPL. */
|
|
target_slot = get_curr_slot(&ab_data);
|
|
if (target_slot == -1) {
|
|
ret = AVB_AB_FLOW_RESULT_ERROR_NO_BOOTABLE_SLOTS;
|
|
printf("No bootable slot found!\n");
|
|
goto out;
|
|
}
|
|
/* Clear the bootloader_verified flag. */
|
|
ab_data.slots[target_slot].bootloader_verified = 0;
|
|
|
|
printf("Verifying slot %s ...\n", slot_suffixes[target_slot]);
|
|
verify_result = avb_slot_verify(ops,
|
|
requested_partitions,
|
|
slot_suffixes[target_slot],
|
|
flags,
|
|
hashtree_error_mode,
|
|
&slot_data);
|
|
|
|
switch (verify_result) {
|
|
case AVB_SLOT_VERIFY_RESULT_ERROR_OOM:
|
|
ret = AVB_AB_FLOW_RESULT_ERROR_OOM;
|
|
goto out;
|
|
|
|
case AVB_SLOT_VERIFY_RESULT_ERROR_IO:
|
|
ret = AVB_AB_FLOW_RESULT_ERROR_IO;
|
|
goto out;
|
|
|
|
case AVB_SLOT_VERIFY_RESULT_OK:
|
|
ret = AVB_AB_FLOW_RESULT_OK;
|
|
break;
|
|
|
|
case AVB_SLOT_VERIFY_RESULT_ERROR_INVALID_METADATA:
|
|
case AVB_SLOT_VERIFY_RESULT_ERROR_UNSUPPORTED_VERSION:
|
|
/* Even with AVB_SLOT_VERIFY_FLAGS_ALLOW_VERIFICATION_ERROR
|
|
* these mean game over.
|
|
*/
|
|
set_slot_unbootable = true;
|
|
break;
|
|
|
|
case AVB_SLOT_VERIFY_RESULT_ERROR_VERIFICATION:
|
|
case AVB_SLOT_VERIFY_RESULT_ERROR_ROLLBACK_INDEX:
|
|
case AVB_SLOT_VERIFY_RESULT_ERROR_PUBLIC_KEY_REJECTED:
|
|
if (flags & AVB_SLOT_VERIFY_FLAGS_ALLOW_VERIFICATION_ERROR) {
|
|
/* Do nothing since we allow this. */
|
|
avb_debugv("Allowing slot ",
|
|
slot_suffixes[target_slot],
|
|
" which verified "
|
|
"with result ",
|
|
avb_slot_verify_result_to_string(verify_result),
|
|
" because "
|
|
"AVB_SLOT_VERIFY_FLAGS_ALLOW_VERIFICATION_ERROR "
|
|
"is set.\n",
|
|
NULL);
|
|
saw_and_allowed_verification_error =
|
|
true;
|
|
} else {
|
|
set_slot_unbootable = true;
|
|
}
|
|
break;
|
|
|
|
case AVB_SLOT_VERIFY_RESULT_ERROR_INVALID_ARGUMENT:
|
|
ret = AVB_AB_FLOW_RESULT_ERROR_INVALID_ARGUMENT;
|
|
goto out;
|
|
/* Do not add a 'default:' case here because
|
|
* of -Wswitch.
|
|
*/
|
|
}
|
|
|
|
if (set_slot_unbootable) {
|
|
avb_errorv("Error verifying slot ",
|
|
slot_suffixes[target_slot],
|
|
" with result ",
|
|
avb_slot_verify_result_to_string(verify_result),
|
|
" - setting unbootable.\n",
|
|
NULL);
|
|
fsl_slot_set_unbootable(&ab_data.slots[target_slot]);
|
|
|
|
/* Only the slot chosen by SPL will be verified here so we
|
|
* return AVB_AB_FLOW_RESULT_ERROR_NO_BOOTABLE_SLOTS if the
|
|
* slot should be set unbootable.
|
|
*/
|
|
ret = AVB_AB_FLOW_RESULT_ERROR_NO_BOOTABLE_SLOTS;
|
|
goto out;
|
|
}
|
|
|
|
/* Update stored rollback index only when the slot has been marked
|
|
* as successful. Do this for every rollback index location.
|
|
*/
|
|
if (ab_data.slots[target_slot].successful_boot != 0) {
|
|
for (n = 0; n < AVB_MAX_NUMBER_OF_ROLLBACK_INDEX_LOCATIONS; n++) {
|
|
|
|
rollback_index_value = slot_data->rollback_indexes[n];
|
|
|
|
if (rollback_index_value != 0) {
|
|
io_ret = ops->read_rollback_index(
|
|
ops, n, ¤t_rollback_index_value);
|
|
if (io_ret == AVB_IO_RESULT_ERROR_OOM) {
|
|
ret = AVB_AB_FLOW_RESULT_ERROR_OOM;
|
|
goto out;
|
|
} else if (io_ret != AVB_IO_RESULT_OK) {
|
|
avb_error("Error getting rollback index for slot.\n");
|
|
ret = AVB_AB_FLOW_RESULT_ERROR_IO;
|
|
goto out;
|
|
}
|
|
if (current_rollback_index_value != rollback_index_value) {
|
|
io_ret = ops->write_rollback_index(
|
|
ops, n, rollback_index_value);
|
|
if (io_ret == AVB_IO_RESULT_ERROR_OOM) {
|
|
ret = AVB_AB_FLOW_RESULT_ERROR_OOM;
|
|
goto out;
|
|
} else if (io_ret != AVB_IO_RESULT_OK) {
|
|
avb_error("Error setting stored rollback index.\n");
|
|
ret = AVB_AB_FLOW_RESULT_ERROR_IO;
|
|
goto out;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Finally, select this slot. */
|
|
avb_assert(slot_data != NULL);
|
|
data = slot_data;
|
|
slot_data = NULL;
|
|
if (saw_and_allowed_verification_error) {
|
|
avb_assert(
|
|
flags & AVB_SLOT_VERIFY_FLAGS_ALLOW_VERIFICATION_ERROR);
|
|
ret = AVB_AB_FLOW_RESULT_OK_WITH_VERIFICATION_ERROR;
|
|
} else {
|
|
ret = AVB_AB_FLOW_RESULT_OK;
|
|
}
|
|
|
|
out:
|
|
io_ret = fsl_save_metadata_if_changed(ab_ops, &ab_data, &ab_data_orig);
|
|
if (io_ret != AVB_IO_RESULT_OK) {
|
|
if (io_ret == AVB_IO_RESULT_ERROR_OOM) {
|
|
ret = AVB_AB_FLOW_RESULT_ERROR_OOM;
|
|
} else {
|
|
ret = AVB_AB_FLOW_RESULT_ERROR_IO;
|
|
}
|
|
if (data != NULL) {
|
|
avb_slot_verify_data_free(data);
|
|
data = NULL;
|
|
}
|
|
}
|
|
|
|
if (slot_data != NULL)
|
|
avb_slot_verify_data_free(slot_data);
|
|
|
|
if (out_data != NULL) {
|
|
*out_data = data;
|
|
} else {
|
|
if (data != NULL) {
|
|
avb_slot_verify_data_free(data);
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
#else /* CONFIG_DUAL_BOOTLOADER */
|
|
/* For legacy i.mx6/7, we won't enable A/B due to the limitation of
|
|
* storage capacity, but we still want to verify boot/recovery with
|
|
* AVB. */
|
|
AvbABFlowResult avb_single_flow(AvbABOps* ab_ops,
|
|
const char* const* requested_partitions,
|
|
AvbSlotVerifyFlags flags,
|
|
AvbHashtreeErrorMode hashtree_error_mode,
|
|
AvbSlotVerifyData** out_data) {
|
|
AvbOps* ops = ab_ops->ops;
|
|
AvbSlotVerifyData* slot_data = NULL;
|
|
AvbSlotVerifyData* data = NULL;
|
|
AvbABFlowResult ret;
|
|
bool saw_and_allowed_verification_error = false;
|
|
|
|
/* Validate boot/recovery. */
|
|
AvbSlotVerifyResult verify_result;
|
|
|
|
verify_result = avb_slot_verify(ops,
|
|
requested_partitions,
|
|
"",
|
|
flags,
|
|
hashtree_error_mode,
|
|
&slot_data);
|
|
switch (verify_result) {
|
|
case AVB_SLOT_VERIFY_RESULT_ERROR_OOM:
|
|
ret = AVB_AB_FLOW_RESULT_ERROR_OOM;
|
|
goto out;
|
|
|
|
case AVB_SLOT_VERIFY_RESULT_ERROR_IO:
|
|
ret = AVB_AB_FLOW_RESULT_ERROR_IO;
|
|
goto out;
|
|
|
|
case AVB_SLOT_VERIFY_RESULT_OK:
|
|
break;
|
|
|
|
case AVB_SLOT_VERIFY_RESULT_ERROR_INVALID_METADATA:
|
|
case AVB_SLOT_VERIFY_RESULT_ERROR_UNSUPPORTED_VERSION:
|
|
/* Even with AVB_SLOT_VERIFY_FLAGS_ALLOW_VERIFICATION_ERROR
|
|
* these mean game over.
|
|
*/
|
|
ret = AVB_AB_FLOW_RESULT_ERROR_NO_BOOTABLE_SLOTS;
|
|
goto out;
|
|
|
|
/* explicit fallthrough. */
|
|
case AVB_SLOT_VERIFY_RESULT_ERROR_VERIFICATION:
|
|
case AVB_SLOT_VERIFY_RESULT_ERROR_ROLLBACK_INDEX:
|
|
case AVB_SLOT_VERIFY_RESULT_ERROR_PUBLIC_KEY_REJECTED:
|
|
if (flags & AVB_SLOT_VERIFY_FLAGS_ALLOW_VERIFICATION_ERROR) {
|
|
/* Do nothing since we allow this. */
|
|
avb_debugv("Allowing slot ",
|
|
slot_suffixes[n],
|
|
" which verified "
|
|
"with result ",
|
|
avb_slot_verify_result_to_string(verify_result),
|
|
" because "
|
|
"AVB_SLOT_VERIFY_FLAGS_ALLOW_VERIFICATION_ERROR "
|
|
"is set.\n",
|
|
NULL);
|
|
saw_and_allowed_verification_error = true;
|
|
} else {
|
|
ret = AVB_AB_FLOW_RESULT_ERROR_NO_BOOTABLE_SLOTS;
|
|
goto out;
|
|
}
|
|
break;
|
|
|
|
case AVB_SLOT_VERIFY_RESULT_ERROR_INVALID_ARGUMENT:
|
|
ret = AVB_AB_FLOW_RESULT_ERROR_INVALID_ARGUMENT;
|
|
goto out;
|
|
/* Do not add a 'default:' case here because of -Wswitch. */
|
|
}
|
|
|
|
avb_assert(slot_data != NULL);
|
|
data = slot_data;
|
|
slot_data = NULL;
|
|
if (saw_and_allowed_verification_error) {
|
|
avb_assert(flags & AVB_SLOT_VERIFY_FLAGS_ALLOW_VERIFICATION_ERROR);
|
|
ret = AVB_AB_FLOW_RESULT_OK_WITH_VERIFICATION_ERROR;
|
|
} else {
|
|
ret = AVB_AB_FLOW_RESULT_OK;
|
|
}
|
|
|
|
out:
|
|
if (slot_data != NULL) {
|
|
avb_slot_verify_data_free(slot_data);
|
|
}
|
|
|
|
if (out_data != NULL) {
|
|
*out_data = data;
|
|
} else {
|
|
if (data != NULL) {
|
|
avb_slot_verify_data_free(data);
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
AvbABFlowResult avb_ab_flow_fast(AvbABOps* ab_ops,
|
|
const char* const* requested_partitions,
|
|
AvbSlotVerifyFlags flags,
|
|
AvbHashtreeErrorMode hashtree_error_mode,
|
|
AvbSlotVerifyData** out_data) {
|
|
AvbOps* ops = ab_ops->ops;
|
|
AvbSlotVerifyData* slot_data[2] = {NULL, NULL};
|
|
AvbSlotVerifyData* data = NULL;
|
|
AvbABFlowResult ret;
|
|
AvbABData ab_data, ab_data_orig;
|
|
size_t slot_index_to_boot, n;
|
|
AvbIOResult io_ret;
|
|
bool saw_and_allowed_verification_error = false;
|
|
size_t target_slot;
|
|
AvbSlotVerifyResult verify_result;
|
|
bool set_slot_unbootable = false;
|
|
uint64_t rollback_index_value = 0;
|
|
uint64_t current_rollback_index_value = 0;
|
|
|
|
io_ret = fsl_load_metadata(ab_ops, &ab_data, &ab_data_orig);
|
|
if (io_ret == AVB_IO_RESULT_ERROR_OOM) {
|
|
ret = AVB_AB_FLOW_RESULT_ERROR_OOM;
|
|
goto out;
|
|
} else if (io_ret != AVB_IO_RESULT_OK) {
|
|
ret = AVB_AB_FLOW_RESULT_ERROR_IO;
|
|
goto out;
|
|
}
|
|
|
|
slot_index_to_boot = 2; // Means not 0 or 1
|
|
target_slot =
|
|
(ab_data.slots[1].priority > ab_data.slots[0].priority) ? 1 : 0;
|
|
|
|
for (n = 0; n < 2; n++) {
|
|
if (!fsl_slot_is_bootable(&ab_data.slots[target_slot])) {
|
|
target_slot = (target_slot == 1 ? 0 : 1);
|
|
continue;
|
|
}
|
|
verify_result = avb_slot_verify(ops,
|
|
requested_partitions,
|
|
slot_suffixes[target_slot],
|
|
flags,
|
|
hashtree_error_mode,
|
|
&slot_data[target_slot]);
|
|
switch (verify_result) {
|
|
case AVB_SLOT_VERIFY_RESULT_ERROR_OOM:
|
|
ret = AVB_AB_FLOW_RESULT_ERROR_OOM;
|
|
goto out;
|
|
|
|
case AVB_SLOT_VERIFY_RESULT_ERROR_IO:
|
|
ret = AVB_AB_FLOW_RESULT_ERROR_IO;
|
|
goto out;
|
|
|
|
case AVB_SLOT_VERIFY_RESULT_OK:
|
|
slot_index_to_boot = target_slot;
|
|
n = 2;
|
|
break;
|
|
|
|
case AVB_SLOT_VERIFY_RESULT_ERROR_INVALID_METADATA:
|
|
case AVB_SLOT_VERIFY_RESULT_ERROR_UNSUPPORTED_VERSION:
|
|
/* Even with AVB_SLOT_VERIFY_FLAGS_ALLOW_VERIFICATION_ERROR
|
|
* these mean game over.
|
|
*/
|
|
set_slot_unbootable = true;
|
|
break;
|
|
|
|
/* explicit fallthrough. */
|
|
case AVB_SLOT_VERIFY_RESULT_ERROR_VERIFICATION:
|
|
case AVB_SLOT_VERIFY_RESULT_ERROR_ROLLBACK_INDEX:
|
|
case AVB_SLOT_VERIFY_RESULT_ERROR_PUBLIC_KEY_REJECTED:
|
|
if (flags & AVB_SLOT_VERIFY_FLAGS_ALLOW_VERIFICATION_ERROR) {
|
|
/* Do nothing since we allow this. */
|
|
avb_debugv("Allowing slot ",
|
|
slot_suffixes[target_slot],
|
|
" which verified "
|
|
"with result ",
|
|
avb_slot_verify_result_to_string(verify_result),
|
|
" because "
|
|
"AVB_SLOT_VERIFY_FLAGS_ALLOW_VERIFICATION_ERROR "
|
|
"is set.\n",
|
|
NULL);
|
|
saw_and_allowed_verification_error =
|
|
true;
|
|
slot_index_to_boot = target_slot;
|
|
n = 2;
|
|
} else {
|
|
set_slot_unbootable = true;
|
|
}
|
|
break;
|
|
|
|
case AVB_SLOT_VERIFY_RESULT_ERROR_INVALID_ARGUMENT:
|
|
ret = AVB_AB_FLOW_RESULT_ERROR_INVALID_ARGUMENT;
|
|
goto out;
|
|
/* Do not add a 'default:' case here because
|
|
* of -Wswitch.
|
|
*/
|
|
}
|
|
|
|
if (set_slot_unbootable) {
|
|
avb_errorv("Error verifying slot ",
|
|
slot_suffixes[target_slot],
|
|
" with result ",
|
|
avb_slot_verify_result_to_string(verify_result),
|
|
" - setting unbootable.\n",
|
|
NULL);
|
|
fsl_slot_set_unbootable(&ab_data.slots[target_slot]);
|
|
set_slot_unbootable = false;
|
|
}
|
|
/* switch to another slot */
|
|
target_slot = (target_slot == 1 ? 0 : 1);
|
|
}
|
|
|
|
if (slot_index_to_boot == 2) {
|
|
/* No bootable slots! */
|
|
avb_error("No bootable slots found.\n");
|
|
ret = AVB_AB_FLOW_RESULT_ERROR_NO_BOOTABLE_SLOTS;
|
|
goto out;
|
|
}
|
|
|
|
/* Update stored rollback index only when the slot has been marked
|
|
* as successful. Do this for every rollback index location.
|
|
*/
|
|
if (ab_data.slots[slot_index_to_boot].successful_boot != 0) {
|
|
for (n = 0; n < AVB_MAX_NUMBER_OF_ROLLBACK_INDEX_LOCATIONS; n++) {
|
|
|
|
rollback_index_value = slot_data[slot_index_to_boot]->rollback_indexes[n];
|
|
|
|
if (rollback_index_value != 0) {
|
|
io_ret = ops->read_rollback_index(
|
|
ops, n, ¤t_rollback_index_value);
|
|
if (io_ret == AVB_IO_RESULT_ERROR_OOM) {
|
|
ret = AVB_AB_FLOW_RESULT_ERROR_OOM;
|
|
goto out;
|
|
} else if (io_ret != AVB_IO_RESULT_OK) {
|
|
avb_error("Error getting rollback index for slot.\n");
|
|
ret = AVB_AB_FLOW_RESULT_ERROR_IO;
|
|
goto out;
|
|
}
|
|
if (current_rollback_index_value != rollback_index_value) {
|
|
io_ret = ops->write_rollback_index(
|
|
ops, n, rollback_index_value);
|
|
if (io_ret == AVB_IO_RESULT_ERROR_OOM) {
|
|
ret = AVB_AB_FLOW_RESULT_ERROR_OOM;
|
|
goto out;
|
|
} else if (io_ret != AVB_IO_RESULT_OK) {
|
|
avb_error("Error setting stored rollback index.\n");
|
|
ret = AVB_AB_FLOW_RESULT_ERROR_IO;
|
|
goto out;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Finally, select this slot. */
|
|
avb_assert(slot_data[slot_index_to_boot] != NULL);
|
|
data = slot_data[slot_index_to_boot];
|
|
slot_data[slot_index_to_boot] = NULL;
|
|
if (saw_and_allowed_verification_error) {
|
|
avb_assert(
|
|
flags & AVB_SLOT_VERIFY_FLAGS_ALLOW_VERIFICATION_ERROR);
|
|
ret = AVB_AB_FLOW_RESULT_OK_WITH_VERIFICATION_ERROR;
|
|
} else {
|
|
ret = AVB_AB_FLOW_RESULT_OK;
|
|
}
|
|
|
|
/* ... and decrement tries remaining, if applicable. */
|
|
if (!ab_data.slots[slot_index_to_boot].successful_boot &&
|
|
(ab_data.slots[slot_index_to_boot].tries_remaining > 0)) {
|
|
ab_data.slots[slot_index_to_boot].tries_remaining -= 1;
|
|
}
|
|
|
|
out:
|
|
io_ret = fsl_save_metadata_if_changed(ab_ops, &ab_data, &ab_data_orig);
|
|
if (io_ret != AVB_IO_RESULT_OK) {
|
|
if (io_ret == AVB_IO_RESULT_ERROR_OOM) {
|
|
ret = AVB_AB_FLOW_RESULT_ERROR_OOM;
|
|
} else {
|
|
ret = AVB_AB_FLOW_RESULT_ERROR_IO;
|
|
}
|
|
if (data != NULL) {
|
|
avb_slot_verify_data_free(data);
|
|
data = NULL;
|
|
}
|
|
}
|
|
|
|
for (n = 0; n < 2; n++) {
|
|
if (slot_data[n] != NULL) {
|
|
avb_slot_verify_data_free(slot_data[n]);
|
|
}
|
|
}
|
|
|
|
if (out_data != NULL) {
|
|
*out_data = data;
|
|
} else {
|
|
if (data != NULL) {
|
|
avb_slot_verify_data_free(data);
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
#endif /* CONFIG_DUAL_BOOTLOADER */
|
|
|
|
#endif /* CONFIG_DUAL_BOOTLOADER && CONFIG_SPL_BUILD */
|