diff --git a/docs/compile.md b/docs/compile.md index c188b68c..c423e758 100644 --- a/docs/compile.md +++ b/docs/compile.md @@ -413,6 +413,12 @@ The tool needs no configuration to match the loader's layout and no rebuild per - **Raw partitions only.** A `DISK_FS` slot is a file inside a filesystem and has no partition tail to claim. - A slot left in `testing` is refused on every path into it, including the failover after another slot fails verification, so it stays out of the boot even in an `ALLOW_DOWNGRADE` build where the version guard is compiled out. +### Golden slot + +`DISK_GOLDEN_SLOT=1` adds a third slot that the disk boot path tries once, after the A/B attempts are spent (both slots invalid, unconfirmed, or failing verification). It is read from `BOOT_PART_GOLDEN` (0-based GPT index, default 2), or by `BOOT_LABEL_GOLDEN` / `BOOT_FILE_GOLDEN` like the A/B slots, and it is fully verified like any other image. Two things differ from A and B. The golden slot is exempt from the anti-rollback check, so a deliberately old recovery image still boots when the newer slots are gone; an anti-rollback refusal of the other update slot falls through to the golden slot instead of halting. And it is never written: no boot-confirmation trailer is ever armed on it, so its state cannot drift. Reaching it is announced on the console (`Falling back to the golden image on pN`). + +The golden partition must be its own partition: a `BOOT_PART_GOLDEN` equal to `BOOT_PART_A` or `BOOT_PART_B` is rejected at build time, and a label that resolves to an A/B partition is rejected when the fallback is reached, since the same image under a third name recovers nothing (`cm4_emmc_rauc.config` shares one partition between A and B and needs an explicit golden partition). The slot is only prepared when the fallback is taken, so a boot that A or B satisfies never reads it; with `DISK_FS` that is also when it is mounted. When neither A nor B carries a usable version, the loader goes to the golden slot directly instead of trying them. + ### Disk boot from a read-only filesystem (FAT32 / ext4) Targets that boot from a disk (`DISK_SDCARD=1`, `DISK_EMMC=1`, or an x86 FSP/AHCI target) use `src/update_disk.c`, which by default reads the signed image from **raw offset 0 of a partition**: the image has to be written there with `dd`, and the partition cannot hold anything else. diff --git a/options.mk b/options.mk index 05b67249..77c0af94 100644 --- a/options.mk +++ b/options.mk @@ -835,6 +835,20 @@ ifeq ($(DISK_BOOT_CONFIRM),1) CFLAGS+=-D"DISK_BOOT_CONFIRM=1" endif +# Optional golden slot for disk boot (src/update_disk.c): tried once after the +# A/B attempts are spent, never written, exempt from anti-rollback. The +# partition is BOOT_PART_GOLDEN (default 2), or BOOT_LABEL_GOLDEN / BOOT_FILE_GOLDEN +# like the A/B slots. +DISK_GOLDEN_SLOT ?= 0 +ifeq ($(DISK_GOLDEN_SLOT),1) + ifeq ($(WOLFBOOT_TARGET_BUILD),1) + ifeq (,$(findstring WOLFBOOT_UPDATE_DISK,$(CFLAGS))) + $(error DISK_GOLDEN_SLOT requires a disk-boot target (DISK_SDCARD=1, DISK_EMMC=1, or an x86 FSP/AHCI target)) + endif + endif + CFLAGS+=-D"DISK_GOLDEN_SLOT=1" +endif + # Optional read-only filesystem support for disk boot (src/update_disk.c), # so a boot slot can name a file instead of requiring the signed image at # raw offset 0 of a partition. DISK_FS = fat32 | ext4 | both. Leaving it diff --git a/src/update_disk.c b/src/update_disk.c index d9fe4594..5488484d 100644 --- a/src/update_disk.c +++ b/src/update_disk.c @@ -123,6 +123,37 @@ static uint8_t disk_encrypt_nonce[ENCRYPT_NONCE_SIZE]; #define MAX_FAILURES 4 #endif +/* Optional last-resort slot, tried once after the A/B attempts are spent. + * It is never written and is exempt from anti-rollback: its job is to bring + * a device back when both update slots are gone, which usually means with an + * older image. */ +#ifdef DISK_GOLDEN_SLOT +#ifndef BOOT_PART_GOLDEN +#define BOOT_PART_GOLDEN 2 +#endif +#ifndef BOOT_FILE_GOLDEN +#define BOOT_FILE_GOLDEN NULL +#endif +#ifndef BOOT_LABEL_GOLDEN +#define BOOT_LABEL_GOLDEN NULL +#endif +/* A golden slot that is one of the A/B partitions is the same image under a + * third name and recovers nothing; configs that share one partition between + * A and B (cm4_emmc_rauc) must point the golden slot elsewhere. Labels are + * resolved at run time and checked there. */ +#if (BOOT_PART_GOLDEN == BOOT_PART_A) || (BOOT_PART_GOLDEN == BOOT_PART_B) +#error "BOOT_PART_GOLDEN must name a partition other than BOOT_PART_A/B" +#endif +#define SLOT_GOLDEN 2 +#define DISK_SLOTS 3 +#define GOLDEN_ATTEMPTS 1 +#define SLOT_IS_GOLDEN(s) ((s) == SLOT_GOLDEN) +#else +#define DISK_SLOTS 2 +#define GOLDEN_ATTEMPTS 0 +#define SLOT_IS_GOLDEN(s) 0 +#endif + #ifndef DISK_BLOCK_SIZE #define DISK_BLOCK_SIZE 512 #endif @@ -324,7 +355,7 @@ struct boot_slot { /* File scope, not stack: several disk targets build with * WOLFBOOT_SMALL_STACK=1. */ -static struct boot_slot boot_slots[2]; +static struct boot_slot boot_slots[DISK_SLOTS]; #ifdef DISK_BOOT_CONFIRM /* Boot confirmation for the disk path. include/disk_trailer.h owns the @@ -336,7 +367,7 @@ static struct boot_slot boot_slots[2]; static uint8_t disk_trailer[DISK_TRAILER_SZ]; /* Last known state of each slot, filled in by disk_boot_state_reap(). */ -static uint8_t slot_state[2]; +static uint8_t slot_state[DISK_SLOTS]; /* Byte offset of the trailer within the slot's partition, or -1 when the slot * cannot carry one. */ @@ -753,13 +784,19 @@ void RAMFUNCTION wolfBoot_start(void) #endif if ((pB_ver == 0) && (pA_ver == 0)) { + wolfBoot_printf("No valid OS image found in either partition %d or %d\r\n", + boot_slots[0].part, boot_slots[1].part); +#ifdef DISK_GOLDEN_SLOT + /* Straight to the golden slot. A version-0 header is not a bootable + * A/B image here any more than it was before this option existed. */ + failures = MAX_FAILURES; +#else #ifdef DISK_ENCRYPT disk_decrypted_header_clear(dec_hdr); disk_crypto_clear(); #endif - wolfBoot_printf("No valid OS image found in either partition %d or %d\r\n", - boot_slots[0].part, boot_slots[1].part); wolfBoot_panic(); +#endif } if (pA_ver != 0U) @@ -776,6 +813,25 @@ void RAMFUNCTION wolfBoot_start(void) do { failures++; +#ifdef DISK_GOLDEN_SLOT + if (failures > MAX_FAILURES) { + selected = SLOT_GOLDEN; + /* Prepared only now, so a boot that A or B satisfies never + * touches the recovery partition (with DISK_FS, preparing + * mounts it and opens the image). */ + (void)slot_prepare(&boot_slots[SLOT_GOLDEN], BOOT_PART_GOLDEN, + BOOT_LABEL_GOLDEN, BOOT_FILE_GOLDEN, slot_max); + if ((boot_slots[SLOT_GOLDEN].part == boot_slots[0].part) || + (boot_slots[SLOT_GOLDEN].part == boot_slots[1].part)) { + wolfBoot_printf("Golden slot p%d is an A/B slot; " + "nothing to recover from\r\n", + boot_slots[SLOT_GOLDEN].part); + boot_slots[SLOT_GOLDEN].ready = 0; + } + wolfBoot_printf("Falling back to the golden image on p%d\r\n", + boot_slots[SLOT_GOLDEN].part); + } +#endif slot = &boot_slots[selected]; cur_part = (uint32_t)slot->part; #ifdef DISK_BOOT_CONFIRM @@ -785,7 +841,8 @@ void RAMFUNCTION wolfBoot_start(void) * with ALLOW_DOWNGRADE defined the version guard that would other- * wise refuse it is compiled out. Refuse it here so the exclusion * holds on every path into the slot, not just the first choice. */ - if (slot_state[selected] == DISK_STATE_TESTING) { + if (!SLOT_IS_GOLDEN(selected) && + slot_state[selected] == DISK_STATE_TESTING) { wolfBoot_printf("Slot %c was not confirmed; not booting it\r\n", 'A' + selected); selected ^= 1; @@ -795,19 +852,26 @@ void RAMFUNCTION wolfBoot_start(void) #ifndef ALLOW_DOWNGRADE { uint32_t cur_ver = selected ? pB_ver_u : pA_ver_u; - if ((max_ver > 0U) && (cur_ver < max_ver)) { + if (!SLOT_IS_GOLDEN(selected) && (max_ver > 0U) && + (cur_ver < max_ver)) { wolfBoot_printf("Rollback to lower version not allowed\r\n"); +#ifdef DISK_GOLDEN_SLOT + /* Nothing newer can boot: only the golden slot is left. */ + failures = MAX_FAILURES; + continue; +#else #ifdef DISK_ENCRYPT disk_decrypted_header_clear(dec_hdr); disk_crypto_clear(); #endif wolfBoot_panic(); return; +#endif } } #endif - part_name[2] = 'A' + selected; + part_name[2] = SLOT_IS_GOLDEN(selected) ? 'G' : ('A' + selected); /* Consumed by the boot traces, which compile out without DEBUG_UART */ (void)cur_part; (void)part_name; @@ -968,7 +1032,7 @@ void RAMFUNCTION wolfBoot_start(void) failures = 0; break; /* Skip verification, boot directly */ #endif - } while (failures < MAX_FAILURES); + } while (failures < MAX_FAILURES + GOLDEN_ATTEMPTS); if (failures) { #ifdef DISK_ENCRYPT @@ -993,7 +1057,8 @@ void RAMFUNCTION wolfBoot_start(void) * so enabling this cannot strand a system whose OS does not confirm. A * slot already SUCCESS, or never written, is left untouched and a * steady-state boot writes nothing at all. */ - if (slot_state[selected] == DISK_STATE_UPDATING) { + if (!SLOT_IS_GOLDEN(selected) && + slot_state[selected] == DISK_STATE_UPDATING) { if (slot_state_write(&boot_slots[selected], DISK_STATE_TESTING, (uint64_t)IMAGE_HEADER_SIZE + (uint64_t)os_image.fw_size) != 0) { diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index 403a0cfc..66de49a6 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -68,7 +68,7 @@ TESTS:=unit-parser unit-parser-large-header unit-fdt unit-extflash unit-string \ unit-update-flash-self-update \ unit-nsc-update \ unit-update-flash-enc unit-update-flash-enc-full unit-update-ram unit-update-ram-uboot unit-update-ram-enc unit-update-ram-enc-nopart unit-update-ram-nofixed unit-update-ram-nofixed-noramboot unit-update-ram-noramboot unit-update-ram-custom-trailer unit-custom-trailer-nopart unit-update-flash-hwswap unit-pkcs11_store unit-psa_store unit-wolfhsm_flash_hal unit-disk \ - unit-update-disk unit-update-disk-confirm unit-update-disk-fsp unit-update-disk-oob unit-update-disk-fit unit-multiboot unit-boot-x86-fsp unit-loader-tpm-init unit-qspi-flash unit-fwtpm-stub unit-tpm-rsa-exp \ + unit-update-disk unit-update-disk-confirm unit-update-disk-golden unit-update-disk-fsp unit-update-disk-oob unit-update-disk-fit unit-multiboot unit-boot-x86-fsp unit-loader-tpm-init unit-qspi-flash unit-fwtpm-stub unit-tpm-rsa-exp \ unit-image-nopart unit-image-sha384 unit-image-sha3-384 unit-image-dts \ unit-image-dts-sha384 unit-image-dts-sha3-384 unit-store-sbrk \ unit-tpm-blob unit-policy-create unit-policy-sign unit-rot-auth unit-sdhci-response-bits \ @@ -408,6 +408,8 @@ unit-update-disk:CFLAGS+=-DMOCK_PARTITIONS -DPRINTF_ENABLED -DWOLFBOOT_RAMBOOT_M -DWOLFBOOT_ORIGIN=MOCK_ADDRESS_BOOT -DBOOTLOADER_PARTITION_SIZE=WOLFBOOT_PARTITION_SIZE unit-update-disk-confirm:CFLAGS+=-DMOCK_PARTITIONS -DPRINTF_ENABLED -DWOLFBOOT_RAMBOOT_MAX_SIZE=0x400 \ -DWOLFBOOT_ORIGIN=MOCK_ADDRESS_BOOT -DBOOTLOADER_PARTITION_SIZE=WOLFBOOT_PARTITION_SIZE +unit-update-disk-golden:CFLAGS+=-DMOCK_PARTITIONS -DPRINTF_ENABLED -DWOLFBOOT_RAMBOOT_MAX_SIZE=0x400 \ + -DWOLFBOOT_ORIGIN=MOCK_ADDRESS_BOOT -DBOOTLOADER_PARTITION_SIZE=WOLFBOOT_PARTITION_SIZE # Non-FSP disk-boot OOB regression (CRIT-03). WOLFBOOT_RAMBOOT_MAX_SIZE is the # cap the loader applies to the unauthenticated header fw_size before loading to # RAM, the same bound update_disk.c and update_ram.c enforce. @@ -1104,6 +1106,9 @@ unit-update-disk: ../../include/target.h unit-update-disk.c unit-update-disk-confirm: ../../include/target.h unit-update-disk-confirm.c gcc -o $@ unit-update-disk-confirm.c $(CFLAGS) $(LDFLAGS) +unit-update-disk-golden: ../../include/target.h unit-update-disk-golden.c + gcc -o $@ unit-update-disk-golden.c $(CFLAGS) $(LDFLAGS) + # WOLFBOOT_FSP (x86) boot path of update_disk.c: the low-memory (tolum) # size check must reject a slot and fall back to the other one, like every # other per-slot rejection in the retry loop. diff --git a/tools/unit-tests/unit-update-disk-golden.c b/tools/unit-tests/unit-update-disk-golden.c new file mode 100644 index 00000000..06cbf1dd --- /dev/null +++ b/tools/unit-tests/unit-update-disk-golden.c @@ -0,0 +1,399 @@ +/* unit-update-disk-golden.c + * + * Golden slot of the disk boot path (DISK_GOLDEN_SLOT): tried once after the + * A/B attempts are spent, exempt from anti-rollback, never written. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ +#define WOLFBOOT_UPDATE_DISK +#define WOLFBOOT_SELF_UPDATE_MONOLITHIC +#define RAM_CODE +#define WOLFBOOT_SELF_HEADER +#define EXT_ENCRYPTED +#define ENCRYPT_WITH_CHACHA +#define HAVE_CHACHA +#define IMAGE_HEADER_SIZE 256 +#define BOOT_PART_A 0 +#define BOOT_PART_B 1 +#define BOOT_PART_GOLDEN 2 +#define DISK_GOLDEN_SLOT +#define DISK_BOOT_CONFIRM +#define MOCK_ADDRESS_BOOT 0xCD000000 + +#include +#include +#include +#include + +#include "hal.h" +#include "target.h" +#include "wolfboot/wolfboot.h" +#include "image.h" +#include "loader.h" +#include + +#define TEST_PAYLOAD_SIZE 64 +#define TEST_PART_SIZE (IMAGE_HEADER_SIZE + TEST_PAYLOAD_SIZE + 1024) +#define FILL_A 0xA1 +#define FILL_B 0xB2 +#define FILL_G 0xC3 +#define N_PARTS 3 + +static uint8_t load_buffer[TEST_PART_SIZE]; +#define WOLFBOOT_LOAD_ADDRESS ((uintptr_t)load_buffer) + +static uint8_t part_image[N_PARTS][TEST_PART_SIZE]; +static int mock_reads[N_PARTS]; +static int mock_writes[N_PARTS]; +static int mock_bad_fill[N_PARTS]; /* payload fills that fail integrity */ +static int mock_do_boot_called; +static int mock_booted_fill; +ChaCha chacha; + +static void set_u16_le(uint8_t *dst, uint16_t value) +{ + dst[0] = (uint8_t)(value & 0xFF); + dst[1] = (uint8_t)(value >> 8); +} + +static void set_u32_le(uint8_t *dst, uint32_t value) +{ + dst[0] = (uint8_t)(value & 0xFF); + dst[1] = (uint8_t)((value >> 8) & 0xFF); + dst[2] = (uint8_t)((value >> 16) & 0xFF); + dst[3] = (uint8_t)(value >> 24); +} + +static void build_image(uint8_t *image, uint32_t version, uint8_t fill) +{ + memset(image, 0, TEST_PART_SIZE); + set_u32_le(image, WOLFBOOT_MAGIC); + set_u32_le(image + sizeof(uint32_t), TEST_PAYLOAD_SIZE); + set_u16_le(image + IMAGE_HEADER_OFFSET, HDR_VERSION); + set_u16_le(image + IMAGE_HEADER_OFFSET + sizeof(uint16_t), 4); + set_u32_le(image + IMAGE_HEADER_OFFSET + 2 * sizeof(uint16_t), version); + memset(image + IMAGE_HEADER_SIZE, fill, TEST_PAYLOAD_SIZE); +} + +static void reset_mocks(void) +{ + memset(load_buffer, 0, sizeof(load_buffer)); + build_image(part_image[0], 3, FILL_A); + build_image(part_image[1], 2, FILL_B); + build_image(part_image[2], 1, FILL_G); + memset(mock_reads, 0, sizeof(mock_reads)); + memset(mock_writes, 0, sizeof(mock_writes)); + memset(mock_bad_fill, 0, sizeof(mock_bad_fill)); + mock_do_boot_called = 0; + mock_booted_fill = -1; + wolfBoot_panicked = 0; +} + +static void mark_bad(uint8_t fill) +{ + int i; + for (i = 0; i < N_PARTS; i++) { + if (mock_bad_fill[i] == 0) { + mock_bad_fill[i] = fill; + return; + } + } +} + +int chacha_init(void) +{ + return 0; +} + +int wc_Chacha_SetIV(ChaCha* ctx, const byte* inIv, word32 counter) +{ + (void)ctx; (void)inIv; (void)counter; + return 0; +} + +int wc_Chacha_Process(ChaCha* ctx, byte* output, const byte* input, word32 msglen) +{ + (void)ctx; + memmove(output, input, msglen); + return 0; +} + +void wc_ForceZero(void* mem, size_t len) +{ + volatile uint8_t *p = (volatile uint8_t *)mem; + while (len-- > 0) + *p++ = 0; +} + +int wolfBoot_initialize_encryption(void) +{ + return 0; +} + +int wolfBoot_get_encrypt_key(uint8_t *key, uint8_t *nonce) +{ + memset(key, 0x5A, ENCRYPT_KEY_SIZE); + memset(nonce, 0xC3, ENCRYPT_NONCE_SIZE); + return 0; +} + +int disk_init(int drv) +{ + (void)drv; + return 0; +} + +int disk_open(int drv) +{ + (void)drv; + return 0; +} + +void disk_close(int drv) +{ + (void)drv; +} + +int disk_part_read(int drv, int part, uint64_t off, uint64_t sz, uint8_t *buf) +{ + (void)drv; + if (part < 0 || part >= N_PARTS) + return -1; + if ((off > TEST_PART_SIZE) || (sz > (TEST_PART_SIZE - off))) + return -1; + mock_reads[part]++; + memcpy(buf, part_image[part] + off, (size_t)sz); + return (int)sz; +} + +int disk_part_write(int drv, int part, uint64_t off, uint64_t sz, + const uint8_t *buf) +{ + (void)drv; + if (part < 0 || part >= N_PARTS) + return -1; + if ((off > TEST_PART_SIZE) || (sz > (TEST_PART_SIZE - off))) + return -1; + mock_writes[part]++; + memcpy(part_image[part] + off, buf, (size_t)sz); + return (int)sz; +} + +int disk_part_size(int drv, int part, uint64_t *size) +{ + (void)drv; + if (part < 0 || part >= N_PARTS) + return -1; + *size = TEST_PART_SIZE; + return 0; +} + +int wolfBoot_open_image_address(struct wolfBoot_image* img, uint8_t* image) +{ + uint32_t magic; + uint32_t fw_size; + + memcpy(&magic, image, sizeof(magic)); + if (magic != WOLFBOOT_MAGIC) + return -1; + memset(img, 0, sizeof(*img)); + img->hdr = image; + memcpy(&fw_size, image + sizeof(uint32_t), sizeof(fw_size)); + img->fw_size = fw_size; + img->fw_base = image + IMAGE_HEADER_SIZE; + img->hdr_ok = 1; + return 0; +} + +/* The loader lands the payload itself at WOLFBOOT_LOAD_ADDRESS (the header + * stays in its own buffer), so the fill byte of whatever was loaded is the + * first byte of load_buffer. */ +int wolfBoot_verify_integrity(struct wolfBoot_image* img) +{ + int i; + uint8_t fill = load_buffer[0]; + (void)img; + for (i = 0; i < N_PARTS; i++) { + if (mock_bad_fill[i] != 0 && mock_bad_fill[i] == fill) + return -1; + } + img->sha_ok = 1; + return 0; +} + +int wolfBoot_verify_authenticity(struct wolfBoot_image* img) +{ + img->signature_ok = 1; + return 0; +} + +int wolfBoot_get_dts_size(void *dts_addr, uint32_t capacity) +{ + (void)capacity; (void)dts_addr; + return -1; +} + +void hal_prepare_boot(void) +{ +} + +void do_boot(const uint32_t *address) +{ + mock_do_boot_called++; + mock_booted_fill = ((const uint8_t*)address)[0]; +} + +int hal_flash_protect(haladdr_t address, int len) +{ + (void)address; (void)len; + return 0; +} + +#include "update_disk.c" + +/* Trailer in the partition tail, as include/disk_trailer.h lays it out. */ +static void set_state(int part, uint8_t state) +{ + uint64_t off = 0; + ck_assert_int_eq(disk_trailer_offset(TEST_PART_SIZE, &off), 0); + disk_trailer_encode(part_image[part] + off, state); +} + +START_TEST(test_golden_untouched_when_a_boots) +{ + reset_mocks(); + wolfBoot_start(); + ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_int_eq(mock_do_boot_called, 1); + ck_assert_int_eq(mock_booted_fill, FILL_A); + ck_assert_int_eq(mock_reads[BOOT_PART_GOLDEN], 0); + ck_assert_int_eq(mock_writes[BOOT_PART_GOLDEN], 0); +} +END_TEST + +START_TEST(test_golden_boots_after_a_and_b_fail_despite_lower_version) +{ + reset_mocks(); + mark_bad(FILL_A); + mark_bad(FILL_B); + wolfBoot_start(); + ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_int_eq(mock_do_boot_called, 1); + ck_assert_int_eq(mock_booted_fill, FILL_G); + /* A was tried; B, older than the failed A, is refused by anti-rollback: + * only its header and its state trailer were read, never its payload. */ + ck_assert_int_ge(mock_reads[BOOT_PART_A], 3); + ck_assert_int_le(mock_reads[BOOT_PART_B], 2); +} +END_TEST + +START_TEST(test_golden_boots_when_both_slots_are_blank) +{ + reset_mocks(); + memset(part_image[0], 0, TEST_PART_SIZE); + memset(part_image[1], 0, TEST_PART_SIZE); + wolfBoot_start(); + ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_int_eq(mock_do_boot_called, 1); + ck_assert_int_eq(mock_booted_fill, FILL_G); +} +END_TEST + +START_TEST(test_golden_boots_when_a_is_version_zero) +{ + reset_mocks(); + /* A valid, signed image whose version parses as 0 next to a blank B is + * "no valid image", not a bootable slot with the version guard relaxed. */ + build_image(part_image[0], 0, FILL_A); + memset(part_image[1], 0, TEST_PART_SIZE); + wolfBoot_start(); + ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_int_eq(mock_do_boot_called, 1); + ck_assert_int_eq(mock_booted_fill, FILL_G); + ck_assert_int_eq(wolfBoot_disk_boot_part(), BOOT_PART_GOLDEN); +} +END_TEST + +START_TEST(test_golden_is_verified_and_a_bad_golden_panics) +{ + reset_mocks(); + mark_bad(FILL_A); + mark_bad(FILL_B); + mark_bad(FILL_G); + wolfBoot_start(); + ck_assert_int_gt(wolfBoot_panicked, 0); + ck_assert_int_eq(mock_do_boot_called, 0); + ck_assert_int_ge(mock_reads[BOOT_PART_GOLDEN], 1); +} +END_TEST + +START_TEST(test_golden_is_never_written_and_skips_confirmation) +{ + reset_mocks(); + set_state(BOOT_PART_A, DISK_STATE_UPDATING); + set_state(BOOT_PART_GOLDEN, DISK_STATE_UPDATING); + mark_bad(FILL_A); + mark_bad(FILL_B); + wolfBoot_start(); + ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_int_eq(mock_booted_fill, FILL_G); + ck_assert_int_eq(mock_writes[BOOT_PART_GOLDEN], 0); +} +END_TEST + +START_TEST(test_golden_boots_when_a_is_unconfirmed_and_b_bad) +{ + reset_mocks(); + set_state(BOOT_PART_A, DISK_STATE_TESTING); + mark_bad(FILL_B); + wolfBoot_start(); + ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_int_eq(mock_booted_fill, FILL_G); + ck_assert_int_eq(mock_writes[BOOT_PART_A], 0); +} +END_TEST + +Suite *wolfboot_suite(void) +{ + Suite *s = suite_create("wolfBoot"); + TCase *tc = tcase_create("update-disk-golden"); + + tcase_add_test(tc, test_golden_untouched_when_a_boots); + tcase_add_test(tc, test_golden_boots_after_a_and_b_fail_despite_lower_version); + tcase_add_test(tc, test_golden_boots_when_both_slots_are_blank); + tcase_add_test(tc, test_golden_boots_when_a_is_version_zero); + tcase_add_test(tc, test_golden_is_verified_and_a_bad_golden_panics); + tcase_add_test(tc, test_golden_is_never_written_and_skips_confirmation); + tcase_add_test(tc, test_golden_boots_when_a_is_unconfirmed_and_b_bad); + + suite_add_tcase(s, tc); + return s; +} + +int main(void) +{ + int fails; + Suite *s = wolfboot_suite(); + SRunner *sr = srunner_create(s); + + srunner_run_all(sr, CK_NORMAL); + fails = srunner_ntests_failed(sr); + srunner_free(sr); + return fails; +}