F-13604: Reset image state on each retry in update_ram fallback

wolfBoot_start() zeroed os_image once, before the retry loop, so the
fallback iteration kept the stale img->hdr of the failed partition and
wolfBoot_open_image_address() (which only adopts load_address when
hdr is NULL) re-verified the wrong image. The external header cache
had the same problem: it kept the first image opened.

memset the struct and invalidate the EXT_FLASH header cache at the
top of each loop iteration, per the documented precondition of
wolfBoot_open_image_address().

Add unit-update-ram-nofixed-noramboot: the non-fixed-partition,
non-RAMBOOT (XIP) layout where load_address varies per retry, with
fallback tests both directions plus candidate-selection checks.
pull/892/head
Daniele Lacamera 2026-09-15 02:58:57 +02:00
parent 8eafe1ef76
commit f0ad5692d2
5 changed files with 380 additions and 3 deletions

View File

@ -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);

View File

@ -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)
{

View File

@ -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();

View File

@ -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)

View File

@ -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 <check.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>
#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 <wolfssl/wolfcrypt/settings.h>
#include <wolfssl/wolfcrypt/sha256.h>
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;
}