diff --git a/include/image.h b/include/image.h index 1fdf340e..f5844498 100644 --- a/include/image.h +++ b/include/image.h @@ -1748,6 +1748,11 @@ uint8_t* wolfBoot_peek_image(struct wolfBoot_image *img, uint32_t offset, /* get header type for image */ uint16_t wolfBoot_get_header(struct wolfBoot_image *img, uint16_t type, uint8_t **ptr); +#ifdef EXT_FLASH +/* Drop the cached external image header so the next open reloads it. */ +void wolfBoot_invalidate_hdr_cache(void); +#endif + /* Find the key slot ID based on the SHA hash of the key. */ int keyslot_id_by_sha(const uint8_t *hint); diff --git a/src/image.c b/src/image.c index 24c806d4..1b97f22a 100644 --- a/src/image.c +++ b/src/image.c @@ -1090,6 +1090,18 @@ static uint8_t *fetch_hdr_cpy(struct wolfBoot_image *img) return hdr_cpy; } +/** + * @brief Invalidate the cached external image header. + * + * fetch_hdr_cpy() loads the header of the first image it sees and serves + * it to every later get_header() call. Call this before opening a + * different image so TLV lookups do not read the stale header. + */ +void wolfBoot_invalidate_hdr_cache(void) +{ + hdr_cpy_done = 0; +} + static uint16_t get_header_ext(struct wolfBoot_image *img, uint16_t type, uint8_t **ptr) { diff --git a/src/update_ram.c b/src/update_ram.c index 4b55dec5..73f77e8c 100644 --- a/src/update_ram.c +++ b/src/update_ram.c @@ -320,9 +320,15 @@ void RAMFUNCTION wolfBoot_start(void) uint32_t max_v = (boot_v > update_v) ? boot_v : update_v; #endif /* !ALLOW_DOWNGRADE && WOLFBOOT_FIXED_PARTITIONS */ - memset(&os_image, 0, sizeof(struct wolfBoot_image)); - for (;;) { + /* Each open needs fresh image state: wolfBoot_open_image_address() + * adopts load_address only when hdr is NULL, and the external + * header cache keeps the first image opened, so without this the + * fallback re-verifies the previous partition's header. */ + memset(&os_image, 0, sizeof(struct wolfBoot_image)); +#ifdef EXT_FLASH + wolfBoot_invalidate_hdr_cache(); +#endif #if defined(WOLFBOOT_DUALBOOT) && defined(WOLFBOOT_FIXED_PARTITIONS) if (active < 0) active = wolfBoot_dualboot_candidate(); diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index dfce2e7e..877ea641 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -62,7 +62,7 @@ TESTS:=unit-parser unit-parser-large-header unit-fdt unit-extflash unit-string \ unit-enc-nvm-flagshome unit-delta unit-gzip unit-update-flash unit-update-flash-delta \ unit-update-flash-hook \ unit-update-flash-self-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-noramboot unit-update-flash-hwswap unit-pkcs11_store unit-psa_store unit-wolfhsm_flash_hal unit-disk \ + 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-flash-hwswap unit-pkcs11_store unit-psa_store unit-wolfhsm_flash_hal unit-disk \ unit-update-disk 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 \ @@ -336,6 +336,17 @@ unit-update-ram-nofixed:CFLAGS+=-DMOCK_PARTITIONS -DWOLFBOOT_NO_SIGN \ -DWOLFBOOT_RAMBOOT_MAX_SIZE=WOLFBOOT_PARTITION_SIZE \ -DWOLFBOOT_ORIGIN=MOCK_ADDRESS_BOOT \ -DBOOTLOADER_PARTITION_SIZE=WOLFBOOT_PARTITION_SIZE +# F-13604: same non-fixed-partition layout as unit-update-ram-nofixed but +# without NO_XIP, so WOLFBOOT_USE_RAMBOOT stays off and +# wolfBoot_open_image_address() runs with a varying load_address on every +# retry iteration (the A/B fallback path that must re-open the second +# partition instead of re-verifying the stale header of the first). +unit-update-ram-nofixed-noramboot:CFLAGS+=-DMOCK_PARTITIONS -DWOLFBOOT_NO_SIGN \ + -DUNIT_TEST_AUTH -DWOLFBOOT_HASH_SHA256 -DPRINTF_ENABLED -DEXT_FLASH \ + -DPART_UPDATE_EXT -DPART_SWAP_EXT -DPART_BOOT_EXT -DWOLFBOOT_DUALBOOT \ + -DWOLFBOOT_NO_PARTITIONS -DUNIT_TEST_NO_FIXED_PARTITIONS \ + -DWOLFBOOT_ORIGIN=MOCK_ADDRESS_BOOT \ + -DBOOTLOADER_PARTITION_SIZE=WOLFBOOT_PARTITION_SIZE # Bound the non-FSP disk load to this test's 64-byte load_buffer (TEST_PAYLOAD_SIZE), # the cap update_disk.c now requires; all images here are exactly that size. unit-update-disk:CFLAGS+=-DMOCK_PARTITIONS -DPRINTF_ENABLED -DWOLFBOOT_RAMBOOT_MAX_SIZE=0x40 \ @@ -912,6 +923,9 @@ unit-update-ram-enc-nopart: ../../include/target.h unit-update-ram-enc.c unit-update-ram-nofixed: ../../include/target.h unit-update-ram-nofixed.c gcc -o $@ unit-update-ram-nofixed.c ../../src/image.c $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha256.c $(CFLAGS) $(LDFLAGS) +unit-update-ram-nofixed-noramboot: ../../include/target.h unit-update-ram-nofixed-noramboot.c + gcc -o $@ unit-update-ram-nofixed-noramboot.c ../../src/image.c $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha256.c $(CFLAGS) $(LDFLAGS) + unit-update-ram-noramboot: ../../include/target.h unit-update-ram-noramboot.c gcc -o $@ unit-update-ram-noramboot.c ../../src/image.c $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha256.c $(CFLAGS) $(LDFLAGS) diff --git a/tools/unit-tests/unit-update-ram-nofixed-noramboot.c b/tools/unit-tests/unit-update-ram-nofixed-noramboot.c new file mode 100644 index 00000000..b3f02006 --- /dev/null +++ b/tools/unit-tests/unit-update-ram-nofixed-noramboot.c @@ -0,0 +1,340 @@ +/* unit-update-ram-nofixed-noramboot.c + * + * Reproducer for fallback selection in update_ram.c without fixed + * partitions and without RAMBOOT (XIP): the configuration in which + * wolfBoot_open_image_address() is called with a varying load_address + * on every retry iteration. + * + * Pins F-13604: os_image was zeroed once, before the retry loop, so the + * fallback iteration kept the stale img->hdr of the failed partition + * and re-verified the wrong image. wolfBoot_open_image_address() only + * adopts the address when img->hdr is NULL (documented precondition: + * the struct is memset to 0 before each call), so the second + * partition was never examined and a valid alternate image could not + * boot. + */ +#ifndef WOLFBOOT_HASH_SHA256 + #define WOLFBOOT_HASH_SHA256 +#endif + +#define IMAGE_HEADER_SIZE 256 +#define MOCK_ADDRESS_UPDATE 0xCC000000 +#define MOCK_ADDRESS_BOOT 0xCD000000 +#define MOCK_ADDRESS_SWAP 0xCE000000 +#define NO_FORK 1 + +#include +#include +#include +#include +#include +#include + +#include "target.h" + +#define TEST_SIZE_SMALL 5300 +#define DIGEST_TLV_OFF_IN_HDR 28 +#define STAGE_ADDR_SENTINEL UINTPTR_MAX + +#include "user_settings.h" +#include "wolfboot/wolfboot.h" + +#define wolfBoot_dualboot_candidate_addr wolfBoot_dualboot_candidate_addr_impl +#include "libwolfboot.c" +#undef wolfBoot_dualboot_candidate_addr + +static int dualboot_candidate_addr_calls; + +int wolfBoot_dualboot_candidate_addr(void** addr) +{ + dualboot_candidate_addr_calls++; + ck_assert_msg(dualboot_candidate_addr_calls == 1, + "wolfBoot_dualboot_candidate_addr() called %d times", + dualboot_candidate_addr_calls); + return wolfBoot_dualboot_candidate_addr_impl(addr); +} + +#include "update_ram.c" +#include "unit-mock-flash.c" +#include +#include + +int wolfBoot_staged_ok = 0; +const uint32_t *wolfBoot_stage_address = + (const uint32_t *)(uintptr_t)STAGE_ADDR_SENTINEL; + +void* hal_get_primary_address(void) +{ + return (void *)(uintptr_t)WOLFBOOT_PARTITION_BOOT_ADDRESS; +} + +void* hal_get_update_address(void) +{ + return (void *)(uintptr_t)WOLFBOOT_PARTITION_UPDATE_ADDRESS; +} + +void do_boot(const uint32_t *address) +{ + if (wolfBoot_panicked) + return; + + wolfBoot_staged_ok++; + wolfBoot_stage_address = address; +} + +static int mock_flash_protect_called = 0; +static haladdr_t mock_flash_protect_addr = 0; +static int mock_flash_protect_len = 0; + +int hal_flash_protect(haladdr_t address, int len) +{ + mock_flash_protect_called++; + mock_flash_protect_addr = address; + mock_flash_protect_len = len; + return 0; +} + +static void reset_mock_stats(void) +{ + wolfBoot_panicked = 0; + wolfBoot_staged_ok = 0; + dualboot_candidate_addr_calls = 0; + mock_flash_protect_called = 0; + mock_flash_protect_addr = 0; + mock_flash_protect_len = 0; +} + +static void prepare_flash(void) +{ + int ret; + + ret = mmap_file("/tmp/wolfboot-unit-ext-file-nofixed-noramboot.bin", + (void *)(uintptr_t)MOCK_ADDRESS_UPDATE, + WOLFBOOT_PARTITION_SIZE + IMAGE_HEADER_SIZE, NULL); + ck_assert_int_ge(ret, 0); + ret = mmap_file("/tmp/wolfboot-unit-int-file-nofixed-noramboot.bin", + (void *)(uintptr_t)MOCK_ADDRESS_BOOT, + WOLFBOOT_PARTITION_SIZE + IMAGE_HEADER_SIZE, NULL); + ck_assert_int_ge(ret, 0); + + ext_flash_unlock(); + ext_flash_erase(WOLFBOOT_PARTITION_BOOT_ADDRESS, + WOLFBOOT_PARTITION_SIZE + IMAGE_HEADER_SIZE); + ext_flash_erase(WOLFBOOT_PARTITION_UPDATE_ADDRESS, + WOLFBOOT_PARTITION_SIZE + IMAGE_HEADER_SIZE); + ext_flash_lock(); +} + +static void cleanup_flash(void) +{ + munmap((void *)WOLFBOOT_PARTITION_BOOT_ADDRESS, + WOLFBOOT_PARTITION_SIZE + IMAGE_HEADER_SIZE); + munmap((void *)WOLFBOOT_PARTITION_UPDATE_ADDRESS, + WOLFBOOT_PARTITION_SIZE + IMAGE_HEADER_SIZE); +} + +static int add_payload(uint8_t part, uint32_t version, uint32_t size) +{ + uint32_t word; + uint16_t word16; + int i; + int ret; + uint8_t *base = (uint8_t *)WOLFBOOT_PARTITION_BOOT_ADDRESS; + wc_Sha256 sha; + uint8_t digest[SHA256_DIGEST_SIZE]; + + ret = wc_InitSha256_ex(&sha, NULL, INVALID_DEVID); + if (ret != 0) + return ret; + + if (part == PART_UPDATE) + base = (uint8_t *)WOLFBOOT_PARTITION_UPDATE_ADDRESS; + srandom(part); + + ext_flash_unlock(); + ext_flash_write((uintptr_t)base, "WOLF", 4); + ext_flash_write((uintptr_t)base + 4, (void *)&size, 4); + + word = 4 << 16 | HDR_VERSION; + ext_flash_write((uintptr_t)base + 8, (void *)&word, 4); + ext_flash_write((uintptr_t)base + 12, (void *)&version, 4); + + word = 2 << 16 | HDR_IMG_TYPE; + ext_flash_write((uintptr_t)base + 16, (void *)&word, 4); + word16 = HDR_IMG_TYPE_AUTH_NONE | HDR_IMG_TYPE_APP; + ext_flash_write((uintptr_t)base + 20, (void *)&word16, 2); + + ret = wc_Sha256Update(&sha, base, DIGEST_TLV_OFF_IN_HDR); + if (ret != 0) + return ret; + + size += IMAGE_HEADER_SIZE; + for (i = IMAGE_HEADER_SIZE; i < (int)size; i += 4) { + uint32_t rand_word = (random() << 16) | random(); + ext_flash_write((uintptr_t)base + i, (void *)&rand_word, 4); + } + for (i = IMAGE_HEADER_SIZE; i < (int)size; i += WOLFBOOT_SHA_BLOCK_SIZE) { + int len = WOLFBOOT_SHA_BLOCK_SIZE; + + if (((int)size - i) < len) + len = (int)size - i; + ret = wc_Sha256Update(&sha, base + i, len); + if (ret != 0) + return ret; + } + + ret = wc_Sha256Final(&sha, digest); + if (ret != 0) + return ret; + wc_Sha256Free(&sha); + + word = SHA256_DIGEST_SIZE << 16 | HDR_SHA256; + ext_flash_write((uintptr_t)base + DIGEST_TLV_OFF_IN_HDR, (void *)&word, 4); + ext_flash_write((uintptr_t)base + DIGEST_TLV_OFF_IN_HDR + 4, digest, + SHA256_DIGEST_SIZE); + ext_flash_lock(); + + return 0; +} + +START_TEST(test_invalid_boot_falls_back_to_update) +{ + uint8_t bad_digest[SHA256_DIGEST_SIZE]; + + reset_mock_stats(); + prepare_flash(); + /* BOOT is the newer image but carries a corrupted digest: the + * candidate selection picks it first, and the fallback must boot + * the valid, older UPDATE image. */ + ck_assert_int_eq(add_payload(PART_BOOT, 2, TEST_SIZE_SMALL), 0); + ck_assert_int_eq(add_payload(PART_UPDATE, 1, TEST_SIZE_SMALL), 0); + + memset(bad_digest, 0xBA, sizeof(bad_digest)); + ext_flash_unlock(); + ext_flash_write(WOLFBOOT_PARTITION_BOOT_ADDRESS + DIGEST_TLV_OFF_IN_HDR + 4, + bad_digest, sizeof(bad_digest)); + ext_flash_lock(); + + wolfBoot_start(); + + ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_int_eq(wolfBoot_staged_ok, 1); + ck_assert_uint_eq((uintptr_t)wolfBoot_stage_address, + (uintptr_t)(WOLFBOOT_PARTITION_UPDATE_ADDRESS + IMAGE_HEADER_SIZE)); +#ifndef TZEN + ck_assert_int_eq(mock_flash_protect_called, 1); + ck_assert_uint_eq((uintptr_t)mock_flash_protect_addr, + (uintptr_t)WOLFBOOT_ORIGIN); + ck_assert_int_eq(mock_flash_protect_len, BOOTLOADER_PARTITION_SIZE); +#endif + cleanup_flash(); +} +END_TEST + +START_TEST(test_invalid_update_falls_back_to_boot) +{ + uint8_t bad_digest[SHA256_DIGEST_SIZE]; + + reset_mock_stats(); + prepare_flash(); + /* Mirror of the previous case: the newer UPDATE image is corrupt, + * the fallback must boot the valid, older BOOT image. */ + ck_assert_int_eq(add_payload(PART_BOOT, 1, TEST_SIZE_SMALL), 0); + ck_assert_int_eq(add_payload(PART_UPDATE, 2, TEST_SIZE_SMALL), 0); + + memset(bad_digest, 0xBA, sizeof(bad_digest)); + ext_flash_unlock(); + ext_flash_write(WOLFBOOT_PARTITION_UPDATE_ADDRESS + DIGEST_TLV_OFF_IN_HDR + 4, + bad_digest, sizeof(bad_digest)); + ext_flash_lock(); + + wolfBoot_start(); + + ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_int_eq(wolfBoot_staged_ok, 1); + ck_assert_uint_eq((uintptr_t)wolfBoot_stage_address, + (uintptr_t)(WOLFBOOT_PARTITION_BOOT_ADDRESS + IMAGE_HEADER_SIZE)); +#ifndef TZEN + ck_assert_int_eq(mock_flash_protect_called, 1); + ck_assert_uint_eq((uintptr_t)mock_flash_protect_addr, + (uintptr_t)WOLFBOOT_ORIGIN); + ck_assert_int_eq(mock_flash_protect_len, BOOTLOADER_PARTITION_SIZE); +#endif + cleanup_flash(); +} +END_TEST + +START_TEST(test_candidate_addr_equal_versions_prefers_boot) +{ + void *addr = NULL; + int ret; + + reset_mock_stats(); + prepare_flash(); + ck_assert_int_eq(add_payload(PART_BOOT, 1, TEST_SIZE_SMALL), 0); + ck_assert_int_eq(add_payload(PART_UPDATE, 1, TEST_SIZE_SMALL), 0); + + ret = wolfBoot_dualboot_candidate_addr_impl(&addr); + + ck_assert_int_eq(ret, 0); + ck_assert_ptr_eq(addr, hal_get_primary_address()); + cleanup_flash(); +} +END_TEST + +START_TEST(test_candidate_addr_newer_update_prefers_update) +{ + void *addr = NULL; + int ret; + + reset_mock_stats(); + prepare_flash(); + ck_assert_int_eq(add_payload(PART_BOOT, 1, TEST_SIZE_SMALL), 0); + ck_assert_int_eq(add_payload(PART_UPDATE, 2, TEST_SIZE_SMALL), 0); + + ret = wolfBoot_dualboot_candidate_addr_impl(&addr); + + ck_assert_int_eq(ret, 1); + ck_assert_ptr_eq(addr, hal_get_update_address()); + cleanup_flash(); +} +END_TEST + +static Suite *wolfboot_suite(void) +{ + Suite *s = suite_create("wolfboot-update-ram-nofixed-noramboot"); + TCase *tc = tcase_create("fallback"); + TCase *tc_candidate = tcase_create("candidate_addr"); + + tcase_add_test(tc, test_invalid_boot_falls_back_to_update); + tcase_add_test(tc, test_invalid_update_falls_back_to_boot); + tcase_set_timeout(tc, 5); + suite_add_tcase(s, tc); + + tcase_add_test(tc_candidate, test_candidate_addr_equal_versions_prefers_boot); + tcase_add_test(tc_candidate, test_candidate_addr_newer_update_prefers_update); + tcase_set_timeout(tc_candidate, 5); + suite_add_tcase(s, tc_candidate); + + return s; +} + +int main(int argc, char *argv[]) +{ + int fails; + Suite *s; + SRunner *sr; + + argv0 = strdup(argv[0]); + (void)argc; + + s = wolfboot_suite(); + sr = srunner_create(s); +#if (NO_FORK == 1) + srunner_set_fork_status(sr, CK_NOFORK); +#endif + srunner_run_all(sr, CK_NORMAL); + fails = srunner_ntests_failed(sr); + srunner_free(sr); + return fails; +}