From f0ad5692d2e4dade11104e11d7ece265cda7b77c Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 15 Sep 2026 02:58:57 +0200 Subject: [PATCH 01/49] 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. --- include/image.h | 5 + src/image.c | 12 + src/update_ram.c | 10 +- tools/unit-tests/Makefile | 16 +- .../unit-update-ram-nofixed-noramboot.c | 340 ++++++++++++++++++ 5 files changed, 380 insertions(+), 3 deletions(-) create mode 100644 tools/unit-tests/unit-update-ram-nofixed-noramboot.c 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; +} From 63f1ce7872eb9e4a7c8200d1da7c533466dc16b8 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 15 Sep 2026 08:24:11 +0200 Subject: [PATCH 02/49] F-12922: do not block fallback to a valid image in wolfBoot_start The version gate in the retry loop panicked whenever the selected candidate failed verification and the fallback partition carried a lower version, even though that image is valid and signed. A single corrupted higher-version image bricked the device instead of falling back. The gate only ever fired on the failure path (candidate selection already prefers the higher version), so remove it; the TESTING-state anti-rollback check in wolfBoot_dualboot_candidate() still guards the confirmed-update path. --- src/update_ram.c | 17 ------------ tools/unit-tests/unit-update-ram-noramboot.c | 27 ++++++++++---------- tools/unit-tests/unit-update-ram.c | 19 +++++++++----- 3 files changed, 25 insertions(+), 38 deletions(-) diff --git a/src/update_ram.c b/src/update_ram.c index 73f77e8c..b1b23702 100644 --- a/src/update_ram.c +++ b/src/update_ram.c @@ -314,12 +314,6 @@ void RAMFUNCTION wolfBoot_start(void) * kernel directly. */ uintptr_t bl31_entry = 0; #endif -#if !defined(ALLOW_DOWNGRADE) && defined(WOLFBOOT_FIXED_PARTITIONS) - uint32_t boot_v = wolfBoot_current_firmware_version(); - uint32_t update_v = wolfBoot_update_firmware_version(); - uint32_t max_v = (boot_v > update_v) ? boot_v : update_v; -#endif /* !ALLOW_DOWNGRADE && WOLFBOOT_FIXED_PARTITIONS */ - for (;;) { /* Each open needs fresh image state: wolfBoot_open_image_address() * adopts load_address only when hdr is NULL, and the external @@ -349,17 +343,6 @@ void RAMFUNCTION wolfBoot_start(void) wolfBoot_panic(); break; } -#if !defined(ALLOW_DOWNGRADE) && defined(WOLFBOOT_FIXED_PARTITIONS) - { - uint32_t active_v = (active == PART_UPDATE) ? update_v : boot_v; - if ((max_v > 0U) && (active_v < max_v)) { - wolfBoot_printf("Rollback to lower version not allowed\n"); - wolfBoot_panic(); - break; - } - } -#endif /* !ALLOW_DOWNGRADE && WOLFBOOT_FIXED_PARTITIONS */ - #if defined(WOLFBOOT_DUALBOOT) && defined(WOLFBOOT_FIXED_PARTITIONS) wolfBoot_printf("Trying %s partition at %p\n", active == PART_BOOT ? "Boot" : "Update", source_address); diff --git a/tools/unit-tests/unit-update-ram-noramboot.c b/tools/unit-tests/unit-update-ram-noramboot.c index b8dca806..98114084 100644 --- a/tools/unit-tests/unit-update-ram-noramboot.c +++ b/tools/unit-tests/unit-update-ram-noramboot.c @@ -184,16 +184,15 @@ START_TEST (test_noramboot_sunnyday) { } END_TEST -/* Regression test for F-4410: firmware versions with the high bit set - * (>= 0x80000000) must still feed the anti-rollback guard in wolfBoot_start. +/* Regression test for F-4410 + F-12922: firmware versions with the high + * bit set (>= 0x80000000) must be read without signed-int clamping (the + * two version asserts below), and a failed high-version image must not + * block fallback to the lower-versioned (but valid) UPDATE partition. * - * BOOT carries the higher version but is marked oversize so wolfBoot_open_image() - * rejects it and the boot path falls back to the lower-versioned (but valid) - * UPDATE partition. That downgrade must be denied. Before the fix the versions - * were cast through a signed int and clamped to 0, collapsing max_v to 0 and - * silently bypassing the "(max_v > 0U)" guard, so the lower UPDATE image was - * staged for boot. */ -START_TEST (test_noramboot_highversion_rollback_denied) { + * BOOT carries the higher version but is marked oversize so + * wolfBoot_open_image() rejects it; the boot path must fall back to the + * valid UPDATE image instead of panicking on the version difference. */ +START_TEST (test_noramboot_fallback_to_lower_version) { uint32_t oversize = WOLFBOOT_PARTITION_SIZE; reset_mock_stats(); @@ -212,10 +211,10 @@ START_TEST (test_noramboot_highversion_rollback_denied) { wolfBoot_start(); - /* Rollback to the lower UPDATE version must be denied: wolfBoot panics and - * stages nothing. */ - ck_assert(!wolfBoot_staged_ok); - ck_assert_int_eq(wolfBoot_panicked, 1); + /* A failed high-version boot image must not block fallback to the + * valid lower-version update image (F-12922). */ + ck_assert(wolfBoot_staged_ok); + ck_assert_int_eq(wolfBoot_panicked, 0); cleanup_flash(); } END_TEST @@ -255,7 +254,7 @@ Suite *wolfboot_suite(void) tcase_add_test(sunnyday, test_noramboot_sunnyday); tcase_add_test(ext_short_read, test_noramboot_ext_flash_short_read_rejected); - tcase_add_test(rollback_denied, test_noramboot_highversion_rollback_denied); + tcase_add_test(rollback_denied, test_noramboot_fallback_to_lower_version); suite_add_tcase(s, sunnyday); suite_add_tcase(s, ext_short_read); suite_add_tcase(s, rollback_denied); diff --git a/tools/unit-tests/unit-update-ram.c b/tools/unit-tests/unit-update-ram.c index f96f2aef..62ad476a 100644 --- a/tools/unit-tests/unit-update-ram.c +++ b/tools/unit-tests/unit-update-ram.c @@ -434,9 +434,10 @@ START_TEST (test_invalid_update_type) { ext_flash_lock(); wolfBoot_update_trigger(); wolfBoot_start(); - ck_assert(!wolfBoot_staged_ok); - ck_assert_int_eq(wolfBoot_panicked, 1); - ck_assert_int_eq(get_version_ramloaded(), 2); + /* Failed update must fall back to the valid boot image, not panic. */ + ck_assert(wolfBoot_staged_ok); + ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_int_eq(get_version_ramloaded(), 1); cleanup_flash(); } @@ -453,8 +454,10 @@ START_TEST (test_update_toolarge) { wolfBoot_update_trigger(); wolfBoot_start(); - ck_assert(!wolfBoot_staged_ok); - ck_assert_int_eq(wolfBoot_panicked, 1); + /* Failed update must fall back to the valid boot image, not panic. */ + ck_assert(wolfBoot_staged_ok); + ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_int_eq(get_version_ramloaded(), 1); cleanup_flash(); } @@ -471,8 +474,10 @@ START_TEST (test_invalid_sha) { ext_flash_lock(); wolfBoot_update_trigger(); wolfBoot_start(); - ck_assert(!wolfBoot_staged_ok); - ck_assert_int_eq(wolfBoot_panicked, 1); + /* Failed update must fall back to the valid boot image, not panic. */ + ck_assert(wolfBoot_staged_ok); + ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_int_eq(get_version_ramloaded(), 1); cleanup_flash(); } From 4e01a45e3bae688602339436dfa5e16ffbd923b2 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 15 Sep 2026 08:41:46 +0200 Subject: [PATCH 03/49] F-12881: p1021: check NAND program/erase status and ECC results The write/erase loops discarded the JEDEC status byte from MDR and hal_flash_command ignored the PAR (uncorrectable ECC) and FCT (FCM timeout) bits, so failed programs, erases and uncorrectable reads all reported success. Check P/WPS in the status byte and stop the loop, and fail the command on PAR/FCT. The small-page FCM sequence now ends with the status command (CM3+RSW) so MDR holds a valid status like the large-page path; not verified on real P1021 hardware. config_io_pin now uses a single masked store for CPDIR/ CPPAR (F-12880): the clear-then-set pair could drop a concurrent update to another pin in the same register. F-12880, F-12881, F-12882 --- hal/nxp_p1021.c | 59 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 44 insertions(+), 15 deletions(-) diff --git a/hal/nxp_p1021.c b/hal/nxp_p1021.c index 774be3bc..8052d65a 100644 --- a/hal/nxp_p1021.c +++ b/hal/nxp_p1021.c @@ -336,6 +336,10 @@ enum elbc_amask_sizes { #define NAND_CMD_READSTART 0x30 /* Extended command for large page devices */ +/* NAND device status byte (JEDEC), returned in MDR by the RS/RSW ops */ +#define NAND_STATUS_WPS (1 << 1) /* write protect status */ +#define NAND_STATUS_FAIL (1 << 3) /* P: program/erase fail */ + /* DDR */ /* DDR3: 512MB, 333.333 MHz (666.667 MT/s) */ @@ -689,6 +693,15 @@ static int hal_flash_command(uint8_t iswrite) if (!(ltesr & ELBC_LTESR_CC)) { ret = -1; } + else if (ltesr & ELBC_LTESR_FCT) { + /* a CW/RSW wait timed out: the device never became ready */ + ret = -1; + } + else if (iswrite == 0 && (ltesr & ELBC_LTESR_PAR)) { + /* uncorrectable ECC error during the FCM read: the data in the + * FCM buffer cannot be trusted */ + ret = -1; + } /* clear interrupt */ set32(ELBC_LTESR, ltesr & ELBC_NAND_MASK); @@ -1063,18 +1076,18 @@ static void config_io_pin(uint8_t port, uint8_t pin, int dir, int open_drain, pin_2bit_dir = (uint32_t)(dir << (NUM_OF_PINS - (pin % (NUM_OF_PINS / 2) + 1) * 2)); - /* Setup the direction */ + /* Setup the direction: one masked store - a clear-then-set pair + * would drop a concurrent update to another pin in the same + * register */ tmp_val = (pin > (NUM_OF_PINS / 2) - 1) ? get32(GUTS_CPDIR2(port)) : get32(GUTS_CPDIR1(port)); if (pin > (NUM_OF_PINS / 2) - 1) { - set32(GUTS_CPDIR2(port), ~pin_2bit_mask & tmp_val); - set32(GUTS_CPDIR2(port), pin_2bit_dir | tmp_val); + set32(GUTS_CPDIR2(port), (~pin_2bit_mask & tmp_val) | pin_2bit_dir); } else { - set32(GUTS_CPDIR1(port), ~pin_2bit_mask & tmp_val); - set32(GUTS_CPDIR1(port), pin_2bit_dir | tmp_val); + set32(GUTS_CPDIR1(port), (~pin_2bit_mask & tmp_val) | pin_2bit_dir); } /* Calculate pin location for 1bit mask */ @@ -1089,21 +1102,21 @@ static void config_io_pin(uint8_t port, uint8_t pin, int dir, int open_drain, set32(GUTS_CPODR(port), ~pin_1bit_mask & tmp_val); } - /* Setup the assignment */ + /* Setup the assignment: one masked store (same reason as the + * direction write above) */ tmp_val = (pin > (NUM_OF_PINS/2) - 1) ? get32(GUTS_CPPAR2(port)): get32(GUTS_CPPAR1(port)); pin_2bit_assign = (uint32_t)(assign << (NUM_OF_PINS - (pin % (NUM_OF_PINS / 2) + 1) * 2)); - /* Clear and set 2 bits mask */ if (pin > (NUM_OF_PINS/2) - 1) { - set32(GUTS_CPPAR2(port), ~pin_2bit_mask & tmp_val); - set32(GUTS_CPPAR2(port), pin_2bit_assign | tmp_val); + set32(GUTS_CPPAR2(port), (~pin_2bit_mask & tmp_val) | + pin_2bit_assign); } else { - set32(GUTS_CPPAR1(port), ~pin_2bit_mask & tmp_val); - set32(GUTS_CPPAR1(port), pin_2bit_assign | tmp_val); + set32(GUTS_CPPAR1(port), (~pin_2bit_mask & tmp_val) | + pin_2bit_assign); } } @@ -1641,13 +1654,19 @@ int ext_flash_write(uintptr_t address, const uint8_t *data, int len) page_size = 512; set32(ELBC_FCR, ELBC_FCR_CMD(0, NAND_CMD_READA) | ELBC_FCR_CMD(1, NAND_CMD_PAGE_PROG2) | - ELBC_FCR_CMD(2, NAND_CMD_PAGE_PROG1)); + ELBC_FCR_CMD(2, NAND_CMD_PAGE_PROG1) | + ELBC_FCR_CMD(3, NAND_CMD_STATUS)); + /* the CM3+RSW pair issues the status command after the program + * execute and waits for it, so MDR holds the page status like the + * large page path */ set32(ELBC_FIR, ELBC_FIR_OP(0, ELBC_FIR_OP_CW0) | ELBC_FIR_OP(1, ELBC_FIR_OP_CM2) | ELBC_FIR_OP(2, ELBC_FIR_OP_CA) | ELBC_FIR_OP(3, ELBC_FIR_OP_PA) | ELBC_FIR_OP(4, ELBC_FIR_OP_WB) | - ELBC_FIR_OP(5, ELBC_FIR_OP_CW1)); + ELBC_FIR_OP(5, ELBC_FIR_OP_CW1) | + ELBC_FIR_OP(6, ELBC_FIR_OP_CM3) | + ELBC_FIR_OP(7, ELBC_FIR_OP_RSW)); #endif (void)block_size; /* not used - shown for reference */ @@ -1690,7 +1709,12 @@ int ext_flash_write(uintptr_t address, const uint8_t *data, int len) wolfBoot_printf("write page %d, col %d, status %x\n", page, col, status); #endif - (void)status; + /* P (program fail) or WPS (write protect) set: the page did not + * program. Stop; retrying the same page fails the same way. */ + if (status & (NAND_STATUS_FAIL | NAND_STATUS_WPS)) { + ret = -1; + break; + } address += write_size; pos += write_size; data += write_size; @@ -1851,7 +1875,12 @@ int ext_flash_erase(uintptr_t address, int len) #ifdef DEBUG_EXT_FLASH wolfBoot_printf("erase page %d, status %x\n", page, status); #endif - (void)status; + /* P (erase fail) or WPS (write protect) set: the block did not + * erase. Stop; erasing the same block fails the same way. */ + if (status & (NAND_STATUS_FAIL | NAND_STATUS_WPS)) { + ret = -1; + break; + } address += block_size; len -= block_size; } From b99847624b28422479bc645797943e8b723093a6 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 15 Sep 2026 08:47:28 +0200 Subject: [PATCH 04/49] F-12879: nrf5340: publish SHM status before signaling IPC hal_shm_status_set() triggered the IPC event before storing the magic/status fields, so the peer core could wake on the event, read stale fields, and wait for a second event that never comes. Store the fields first, order them with a DSB, then signal; the receiver pairs it with a DSB after clearing the event, before re-reading the fields. Not verified on real nRF5340 hardware. --- hal/nrf5340.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/hal/nrf5340.c b/hal/nrf5340.c index f1762086..64ae0611 100644 --- a/hal/nrf5340.c +++ b/hal/nrf5340.c @@ -587,11 +587,14 @@ static void hal_shm_init(void) static void hal_shm_status_set(ShmInfo_t* info, uint32_t status) { - IPC_TASKS_SEND(USE_IPC_SEND) = 1; if (info != NULL) { info->magic = SHAREM_MEM_MAGIC; info->status = status; } + /* publish the fields before signaling: the peer reads them right + * after seeing the IPC event */ + DSB(); + IPC_TASKS_SEND(USE_IPC_SEND) = 1; } static uint32_t hal_shm_status_wait(ShmInfo_t* info, uint32_t status, @@ -616,6 +619,9 @@ static uint32_t hal_shm_status_wait(ShmInfo_t* info, uint32_t status, } /* clear event */ IPC_EVENTS_RECEIVE(USE_IPC_RECV) = 0; + /* the sender published the fields before the event: order this + * core's field reads after the event observation */ + DSB(); /* if we got an event and "info" not provided, just return status to * signal event occurred */ if (info == NULL) { From bad522f6549bd9bc16035a94016f765d4642f08c Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 15 Sep 2026 13:24:14 +0200 Subject: [PATCH 05/49] F-12942: pkcs11: wipe PIN on terminal init failure pkcs11_crypto_init() tore down the session on failure but left the pre-populated pkcs11_pin credential in retained bootloader memory after the handoff. Wipe it in the failure path, same as the deinit path (F-12114). Regression test in unit-pkcs11-pin-zeroize: login rejected -> init fails -> every pin byte is zero. --- src/libwolfboot.c | 5 ++++ tools/unit-tests/unit-pkcs11-pin-zeroize.c | 35 +++++++++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/libwolfboot.c b/src/libwolfboot.c index 5569d645..932677a1 100644 --- a/src/libwolfboot.c +++ b/src/libwolfboot.c @@ -2369,6 +2369,8 @@ static int pkcs11_enc_initialized = 0, pkcs11_dec_initialized = 0; static CK_AES_CTR_PARAMS pkcs11_params; #endif +static void pkcs11_pin_wipe(void); + int pkcs11_crypto_init(void) { CK_RV ret = 0; @@ -2462,6 +2464,9 @@ int pkcs11_crypto_init(void) if (pkcs11_initialized) { pkcs11_function_list->C_Finalize(NULL); } + /* terminal failure: the credential must not survive in retained + * memory (same reason as the deinit wipe) */ + pkcs11_pin_wipe(); } return ret; diff --git a/tools/unit-tests/unit-pkcs11-pin-zeroize.c b/tools/unit-tests/unit-pkcs11-pin-zeroize.c index 591cbe25..8c801eb9 100644 --- a/tools/unit-tests/unit-pkcs11-pin-zeroize.c +++ b/tools/unit-tests/unit-pkcs11-pin-zeroize.c @@ -1,6 +1,7 @@ /* unit-pkcs11-pin-zeroize.c * - * Unit test for the PKCS#11 login credential lifetime (F-12114). + * Unit test for the PKCS#11 login credential lifetime (F-12114, + * F-12942). * * pkcs11_pin is a file-scope copy of the credential supplied to * C_Login() for the token holding the firmware-decryption key. @@ -58,6 +59,7 @@ static uint8_t test_encrypt_key[ENCRYPT_PKCS11_KEY_ID_SIZE + /* ---- PKCS#11 stubs ---- */ static int stub_close_session_calls; +static int stub_login_fail; static CK_RV stub_C_Initialize(CK_VOID_PTR pInitArgs) { @@ -98,6 +100,9 @@ static CK_RV stub_C_Login(CK_SESSION_HANDLE hSession, CK_USER_TYPE userType, (void)userType; (void)pPin; (void)ulPinLen; + if (stub_login_fail) { + return CKR_PIN_INCORRECT; + } return CKR_OK; } @@ -171,6 +176,7 @@ void panic(void) static void reset_stub_state(void) { stub_close_session_calls = 0; + stub_login_fail = 0; } /* F-12114: the pre-handoff deinitializer must erase the PKCS#11 @@ -223,6 +229,32 @@ START_TEST(test_pkcs11_deinit_no_session) } END_TEST +/* F-12942: a terminal initialization failure after C_Login() was + * attempted (login rejected) tears the session down and must also + * erase the credential copy: the bootloader memory is retained + * after the handoff, as on the deinit path. */ +START_TEST(test_pkcs11_pin_wiped_on_init_failure) +{ + int ret; + size_t i; + + reset_stub_state(); + encrypt_initialized = 0; + memcpy(pkcs11_pin, ENCRYPT_PKCS11_PIN, sizeof(ENCRYPT_PKCS11_PIN)); + stub_login_fail = 1; + + ret = pkcs11_crypto_init(); + stub_login_fail = 0; + + ck_assert_int_eq(ret, CKR_PIN_INCORRECT); + ck_assert_int_eq(encrypt_initialized, 0); + for (i = 0; i < sizeof(pkcs11_pin); i++) { + ck_assert_msg(pkcs11_pin[i] == 0, + "pkcs11_pin byte %zu not wiped", i); + } +} +END_TEST + Suite *wolfboot_suite(void) { Suite *s = suite_create("wolfboot-pkcs11-pin"); @@ -230,6 +262,7 @@ Suite *wolfboot_suite(void) tcase_add_test(tc, test_pkcs11_pin_wiped_on_deinit); tcase_add_test(tc, test_pkcs11_deinit_no_session); + tcase_add_test(tc, test_pkcs11_pin_wiped_on_init_failure); suite_add_tcase(s, tc); return s; } From efc5753b2831de8c6807419c4c0cd84c794092b7 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 15 Sep 2026 14:32:18 +0200 Subject: [PATCH 06/49] unit-tests: extract NAND_STATUS_* for p1021 erase test ext_flash_erase() now checks NAND_STATUS_FAIL | NAND_STATUS_WPS (F-12881), but the sed extraction rule only pulled the command defines, so the unit_tests CI jobs failed to compile the test. --- tools/unit-tests/Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index 877ea641..22a411d8 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -1332,6 +1332,8 @@ p1021_erase_extract.h: ../../hal/nxp_p1021.c sed -n '/#define NAND_CMD_BLOCK_ERASE1 /p' $< >> $@ sed -n '/#define NAND_CMD_BLOCK_ERASE2 /p' $< >> $@ sed -n '/#define FLASH_PAGE_SIZE /p' $< >> $@ + sed -n '/#define NAND_STATUS_WPS /p' $< >> $@ + sed -n '/#define NAND_STATUS_FAIL /p' $< >> $@ p1021_erase_fn_extract.h: ../../hal/nxp_p1021.c sed -n '/^int ext_flash_erase/,/^}/p' $< > $@ From 2c636e9606862b2ba99c9c3594019ff25b94e892 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 15 Sep 2026 14:32:18 +0200 Subject: [PATCH 07/49] nrf5340: add memory clobber to DSB() The F-12879 fix publishes SHM status with plain C stores before IPC_TASKS_SEND; without a compiler memory barrier an optimized build can reorder the stores past the dsb asm. Matches the existing clobber in imx95_m7.h and stm32g4.h. --- hal/nrf5340.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hal/nrf5340.h b/hal/nrf5340.h index da9bdc87..eb48b0a2 100644 --- a/hal/nrf5340.h +++ b/hal/nrf5340.h @@ -68,7 +68,7 @@ /* Assembly helpers */ #define DMB() __asm__ volatile ("dmb") -#define DSB() __asm__ volatile ("dsb") +#define DSB() __asm__ volatile ("dsb" ::: "memory") #define ISB() __asm__ volatile ("isb") #define NOP() __asm__ volatile ("nop") From e38e84438161e91e74fb3f3a67c610377cf89ec0 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 15 Sep 2026 14:32:18 +0200 Subject: [PATCH 08/49] unit-tests: rename noramboot fallback TCase The F-12922 test checks fallback to the lower version, but the TCase was still named "rollback denied" from before the fix, describing the opposite behavior. --- tools/unit-tests/unit-update-ram-noramboot.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tools/unit-tests/unit-update-ram-noramboot.c b/tools/unit-tests/unit-update-ram-noramboot.c index 98114084..ce3d0de9 100644 --- a/tools/unit-tests/unit-update-ram-noramboot.c +++ b/tools/unit-tests/unit-update-ram-noramboot.c @@ -248,19 +248,20 @@ Suite *wolfboot_suite(void) TCase *sunnyday = tcase_create("Non-RAMBOOT sunny day"); TCase *ext_short_read = tcase_create("Non-RAMBOOT short ext flash read rejected"); - TCase *rollback_denied = - tcase_create("Non-RAMBOOT high-version rollback denied"); + TCase *fallback_to_lower_version = + tcase_create("Non-RAMBOOT fallback to lower version"); tcase_add_test(sunnyday, test_noramboot_sunnyday); tcase_add_test(ext_short_read, test_noramboot_ext_flash_short_read_rejected); - tcase_add_test(rollback_denied, test_noramboot_fallback_to_lower_version); + tcase_add_test(fallback_to_lower_version, + test_noramboot_fallback_to_lower_version); suite_add_tcase(s, sunnyday); suite_add_tcase(s, ext_short_read); - suite_add_tcase(s, rollback_denied); + suite_add_tcase(s, fallback_to_lower_version); tcase_set_timeout(sunnyday, 5); tcase_set_timeout(ext_short_read, 5); - tcase_set_timeout(rollback_denied, 5); + tcase_set_timeout(fallback_to_lower_version, 5); return s; } From b1884b03d7768261c027cc891e364c78e2015c6c Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 15 Sep 2026 15:14:38 +0200 Subject: [PATCH 09/49] F-7394: sign: free RNG on RSA-PSS unsupported-hash error The error path returned before the common wc_FreeRng(), leaving DRBG state on the stack. Route it through the shared cleanup and guard the sign call so hash_type/mgf are only used when set. --- tools/keytools/sign.c | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tools/keytools/sign.c b/tools/keytools/sign.c index 4938e268..6a3d62a7 100644 --- a/tools/keytools/sign.c +++ b/tools/keytools/sign.c @@ -1218,13 +1218,15 @@ static int sign_digest(int sign, int hash_algo, mgf = WC_MGF1SHA384; } else { fprintf(stderr, "RSA-PSS requires SHA-256 or SHA-384\n"); - return -1; + ret = -1; } - ret = wc_RsaPSS_Sign(digest, digest_sz, signature, *signature_sz, - hash_type, mgf, &k->rsa, &rng); - if (ret > 0) { - *signature_sz = ret; - ret = 0; + if (ret == 0) { + ret = wc_RsaPSS_Sign(digest, digest_sz, signature, *signature_sz, + hash_type, mgf, &k->rsa, &rng); + if (ret > 0) { + *signature_sz = ret; + ret = 0; + } } } else From a08badf880fb89e7fe818945a2cd5509b685fb6e Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 15 Sep 2026 15:14:38 +0200 Subject: [PATCH 10/49] F-8008: keygen: free RNG before helper failure exit keygen_rsa/keygen_ecc exit() from their own cleanup labels, bypassing main()'s wc_FreeRng() and leaving DRBG state resident. Add keygen_die() that frees and zeroes the RNG before exit and use it in the two helpers' failure paths. --- tools/keytools/keygen.c | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tools/keytools/keygen.c b/tools/keytools/keygen.c index 3751f588..115b3451 100644 --- a/tools/keytools/keygen.c +++ b/tools/keytools/keygen.c @@ -109,6 +109,15 @@ static int exportPubKey = 0; static WC_RNG rng; static int noLocalKeys = 0; +/* Exit after the RNG has been initialised: free the DRBG state first so + * it is not left resident in process memory, then terminate. */ +static void keygen_die(int code) +{ + wc_FreeRng(&rng); + wc_ForceZero(&rng, sizeof(rng)); + exit(code); +} + /* ML-DSA pub keys are big. */ #define KEYSLOT_MAX_PUBKEY_SIZE ML_DSA_L5_PUBKEY_SIZE @@ -567,7 +576,7 @@ static void keygen_rsa(const char *keyfile, int kbits, uint32_t id_mask, ret = wc_InitRsaKey(&k, NULL); if (ret != 0) { fprintf(stderr, "Unable to initialize RSA%d key\n", kbits); - exit(1); + keygen_die(1); } rsa_init = 1; @@ -616,7 +625,7 @@ cleanup: wc_FreeRsaKey(&k); wc_ForceZero(&k, sizeof(k)); if (exit_code != 0) - exit(exit_code); + keygen_die(exit_code); } #define MAX_ECC_KEY_SIZE 66 @@ -731,7 +740,7 @@ cleanup: wc_ForceZero(priv_der, sizeof(priv_der)); if (exit_code != 0) - exit(exit_code); + keygen_die(exit_code); memcpy(k_buffer, Qx, ecc_key_size); memcpy(k_buffer + ecc_key_size, Qy, ecc_key_size); From 483facf339387bb33ee47538483f1b68a47e2811 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 15 Sep 2026 15:19:25 +0200 Subject: [PATCH 11/49] F-9767: keygen: free RNG before helper failure exit keygen_ed25519/ed448/lms/xmss/ml_dsa have the same bypass as the RSA/ECC helpers: exit() from their cleanup labels skips main()'s wc_FreeRng(). Route their failure exits through keygen_die() as well. --- tools/keytools/keygen.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tools/keytools/keygen.c b/tools/keytools/keygen.c index 115b3451..2dc1f2d9 100644 --- a/tools/keytools/keygen.c +++ b/tools/keytools/keygen.c @@ -812,7 +812,7 @@ cleanup: wc_ed25519_free(&k); wc_ForceZero(&k, sizeof(k)); if (exit_code != 0) - exit(exit_code); + keygen_die(exit_code); } static void keygen_ed448(const char *privkey, uint32_t id_mask) @@ -873,7 +873,7 @@ cleanup: wc_ed448_free(&k); wc_ForceZero(&k, sizeof(k)); if (exit_code != 0) - exit(exit_code); + keygen_die(exit_code); } #include "../lms/lms_common.h" @@ -995,7 +995,7 @@ cleanup: wc_ForceZero(&key, sizeof(key)); } if (exit_code) - exit(exit_code); + keygen_die(exit_code); } #include "../xmss/xmss_common.h" @@ -1114,7 +1114,7 @@ cleanup: wc_ForceZero(&key, sizeof(key)); } if (exit_code) - exit(exit_code); + keygen_die(exit_code); } @@ -1325,7 +1325,7 @@ cleanup: priv = NULL; } if (exit_code != 0) - exit(exit_code); + keygen_die(exit_code); } static void key_gen_check(const char *kfilename) From b97d94deebb7602ab906a321f2365b99955099cb Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 15 Sep 2026 15:21:54 +0200 Subject: [PATCH 12/49] F-9768: rot: scrub NV auth and session state at exit The TPM NV auth copy and the HMAC session state lived on the stack until process teardown. Zeroize both at the common exit label after the TPM device is unloaded, on success and error paths alike. --- tools/tpm/rot.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tools/tpm/rot.c b/tools/tpm/rot.c index 9de7bce1..9f0b03e2 100644 --- a/tools/tpm/rot.c +++ b/tools/tpm/rot.c @@ -204,6 +204,11 @@ exit: wolfTPM2_UnloadHandle(&dev, &tpmSession.handle); wolfTPM2_Cleanup(&dev); + /* Scrub the NV auth copy and the session state from the stack on + * every exit path. */ + wc_ForceZero(&nv, sizeof(nv)); + wc_ForceZero(&tpmSession, sizeof(tpmSession)); + return rc; } From 19d8a42303588d69e58b3400e2e6f9b32351a7ce Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 15 Sep 2026 15:35:10 +0200 Subject: [PATCH 13/49] F-6765: scrub previous value before overwriting PSA storage ARM_TEE_PS_SET on an existing object XMEMCPY'd the new value over the old one without clearing the buffer first, so a SET that stores less data (or zero data) left the tail of the previous object readable through ARM_TEE_PS_GET. Zero the full data area with wc_ForceZero after every validation check passes and before the copy, matching the DELETE path. Reported by Fenrir. --- src/arm_tee_psa_ipc.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/arm_tee_psa_ipc.c b/src/arm_tee_psa_ipc.c index f6244585..63223279 100644 --- a/src/arm_tee_psa_ipc.c +++ b/src/arm_tee_psa_ipc.c @@ -925,6 +925,10 @@ static int32_t arm_tee_psa_ps_dispatch(int32_t type, const psa_invec *in_vec, if (data_len > 0 && data == NULL) { return PSA_ERROR_INVALID_ARGUMENT; } + /* Scrub the previous value before overwriting: a SET that stores + * less data (or zero) must not leave the tail of the old object + * readable via GET. Runs only after every validation check. */ + wc_ForceZero(entry->data, sizeof(entry->data)); if (data_len > 0) { XMEMCPY(entry->data, data, data_len); } From 5f6eb055524ee9f0fe485b03ccd9e3f8f4e5ddd7 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 15 Sep 2026 15:43:11 +0200 Subject: [PATCH 14/49] F-6764: scrub passphrase buffer when async ATA command completes security_command_passphrase() only wiped the static DMA buffer on the synchronous path; in async mode (ata_security_erase_unit) the passphrase stayed resident in the file-static buffer for the rest of the boot, since ata_cmd_complete_async() never scrubbed it. Add a scrub_buffer flag to the async state, set it when the command goes in flight (or wipe immediately if it never started), and scrub in ata_cmd_complete_async() on both the success and task-file-error exits, once the HBA has retired the command. Covered by two new unit tests: scrub after async completion and after an async port error; the in-flight state is asserted unscrubbed. Reported by Fenrir. --- src/x86/ata.c | 35 ++++++++++-- .../unit-ata-security-passphrase-zeroize.c | 53 ++++++++++++++++++- 2 files changed, 83 insertions(+), 5 deletions(-) diff --git a/src/x86/ata.c b/src/x86/ata.c index 5858c4d3..6a00eb27 100644 --- a/src/x86/ata.c +++ b/src/x86/ata.c @@ -61,9 +61,16 @@ struct ata_async_info{ int in_progress; int drv; int slot; + /* 1 when the in-flight async command carries a passphrase in the + * static DMA buffer and it must be scrubbed at completion. */ + int scrub_buffer; }; static struct ata_async_info ata_async_info; + +#ifdef WOLFBOOT_ATA_DISK_LOCK +static void ata_security_buffer_zeroize(void); +#endif /** * @brief This structure holds the necessary information for an ATA drive, * including AHCI base address, AHCI port number, and sector cache. @@ -286,21 +293,33 @@ int ata_cmd_complete_async() { struct ata_drive *ata; int slot; + int ret; if (!ata_async_info.in_progress) return ATA_ERR_OP_NOT_IN_PROGRESS; ata = &ATA_Drv[ata_async_info.drv]; if (mmio_read32(AHCI_PxIS(ata->ahci_base, ata->ahci_port)) & AHCI_PORT_IS_TFES) { - ata_async_info.in_progress = 0; - return -1; + ret = -1; + goto done; } slot = ata_async_info.slot; if ((mmio_read32(AHCI_PxCI(ata->ahci_base, ata->ahci_port)) & (1 << slot)) != 0) return ATA_ERR_BUSY; + ret = 0; +done: + /* The HBA has retired the command (success or task-file error), so + * the static DMA buffer is no longer in flight: scrub the passphrase + * it carried before it can be read back from SRAM. */ ata_async_info.in_progress = 0; - return 0; +#ifdef WOLFBOOT_ATA_DISK_LOCK + if (ata_async_info.scrub_buffer) { + ata_async_info.scrub_buffer = 0; + ata_security_buffer_zeroize(); + } +#endif + return ret; } /** @@ -502,8 +521,16 @@ static int security_command_passphrase(int drv, uint8_t ata_cmd, * may still be in flight when we return (the caller polls completion * via ata_cmd_complete_async()), so clearing the buffer now would race * the HBA and could corrupt the command still in progress. */ - if (!async) + if (!async) { ata_security_buffer_zeroize(); + } else if (ret == ATA_ERR_BUSY) { + /* Command is in flight: scrub once the HBA retires it. */ + ata_async_info.scrub_buffer = 1; + } else { + /* Command never started (another async op in progress): the + * buffer is not referenced by any in-flight transfer. */ + ata_security_buffer_zeroize(); + } return ret; } diff --git a/tools/unit-tests/unit-ata-security-passphrase-zeroize.c b/tools/unit-tests/unit-ata-security-passphrase-zeroize.c index f0df0d6c..9541360c 100644 --- a/tools/unit-tests/unit-ata-security-passphrase-zeroize.c +++ b/tools/unit-tests/unit-ata-security-passphrase-zeroize.c @@ -12,6 +12,7 @@ #include #include #include +#include #include #define WOLFBOOT_ATA_DISK_LOCK @@ -25,10 +26,18 @@ * slot" error path. */ static int mock_slots_full; +/* When set, port IS reads report a task-file error so + * ata_cmd_complete_async() takes its error exit. */ +static int mock_tfes; + uint32_t mmio_read32(uintptr_t address) { (void)address; - return mock_slots_full ? 0xFFFFFFFF : 0; + if (mock_slots_full) + return 0xFFFFFFFF; + if (mock_tfes) + return AHCI_PORT_IS_TFES; + return 0; } void mmio_write32(uintptr_t address, uint32_t value) @@ -54,6 +63,7 @@ static uint8_t *ctable_mem; static void setup(void) { mock_slots_full = 0; + mock_tfes = 0; clb_mem = mmap(NULL, sizeof(struct hba_cmd_header) * 32, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_32BIT, -1, 0); @@ -120,6 +130,45 @@ START_TEST(test_unlock_zeroizes_passphrase_on_no_free_slot) } END_TEST +START_TEST(test_erase_unit_async_zeroizes_on_completion) +{ + static const char passphrase[] = "unit-test-disk-secret"; + volatile uint8_t *pw = + (volatile uint8_t *)buffer + ATA_SECURITY_PASSWORD_OFFSET; + int r; + int i; + + r = ata_security_erase_unit(0, passphrase, 0); + ck_assert_int_eq(r, ATA_ERR_BUSY); + + /* Command is in flight: scrubbing now would race the HBA DMA, so + * the passphrase must still be present until completion. */ + for (i = 0; i < (int)strlen(passphrase); i++) + ck_assert_uint_eq(pw[i], (uint8_t)passphrase[i]); + + r = ata_cmd_complete_async(); + ck_assert_int_eq(r, 0); + + assert_password_field_zero("after async SECURITY ERASE UNIT completion"); +} +END_TEST + +START_TEST(test_erase_unit_async_zeroizes_on_port_error) +{ + static const char passphrase[] = "unit-test-disk-secret"; + int r; + + r = ata_security_erase_unit(0, passphrase, 0); + ck_assert_int_eq(r, ATA_ERR_BUSY); + + mock_tfes = 1; + r = ata_cmd_complete_async(); + ck_assert_int_eq(r, -1); + + assert_password_field_zero("after async port-error completion"); +} +END_TEST + static Suite *ata_security_passphrase_zeroize_suite(void) { Suite *s = suite_create("ata_security_passphrase_zeroize"); @@ -127,6 +176,8 @@ static Suite *ata_security_passphrase_zeroize_suite(void) tcase_add_checked_fixture(tc, setup, teardown); tcase_add_test(tc, test_unlock_zeroizes_passphrase_after_command_completes); tcase_add_test(tc, test_unlock_zeroizes_passphrase_on_no_free_slot); + tcase_add_test(tc, test_erase_unit_async_zeroizes_on_completion); + tcase_add_test(tc, test_erase_unit_async_zeroizes_on_port_error); suite_add_tcase(s, tc); return s; } From 908d4ec1e395f19ceddf2fae1cb933f47d7767fc Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 15 Sep 2026 15:48:15 +0200 Subject: [PATCH 15/49] F-7048: make UDS all-value scan constant-time buffer_is_all_value() early-exited on the first byte differing from the target, so the loop trip count leaked the length of the leading 0xFF/0x00 run of the UDS, the DICE root secret read in hal_uds_derive_key(). Replace the early exit with a volatile |= accumulator over the full buffer, matching the constant-time compare pattern already used by image_CT_compare() and the other secret comparisons in the tree. Verified: gcc -S -O2 shows a straight-line loop body with only the data-independent i < len branch; arm-none-eabi-gcc (stm32h5 preset) compiles the file clean. Reported by Fenrir. --- hal/stm32h5.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/hal/stm32h5.c b/hal/stm32h5.c index 5cd6b5f9..279edff4 100644 --- a/hal/stm32h5.c +++ b/hal/stm32h5.c @@ -247,14 +247,15 @@ static int uds_from_uid(uint8_t *out, size_t out_len) static int buffer_is_all_value(const uint8_t *buf, size_t len, uint8_t value) { + volatile uint8_t diff = 0U; size_t i; + /* Constant-time scan: the buffer holds the UDS, the DICE root + * secret, so the loop must not early-exit on a data-dependent byte. */ for (i = 0; i < len; i++) { - if (buf[i] != value) { - return 0; - } + diff |= (uint8_t)(buf[i] ^ value); } - return 1; + return diff == 0; } int hal_uds_derive_key(uint8_t *out, size_t out_len) From 4ca67c3063ea28827c801b3a8bbac19bdf86d267 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 15 Sep 2026 16:08:57 +0200 Subject: [PATCH 16/49] F-13615: keep TFM_TIMING_RESISTANT for secure-mode fastmath builds Secure-mode worlds (TZ_PSA/PKCS11/FWTPM/WOLFHSM) process private keys in software, so a USE_FAST_MATH build of one of them must keep the timing-resistant TFM path instead of falling back to WC_NO_HARDEN. The old condition only covered software DICE; FIPS=1 already forces SPMATH (options.mk M10) and all CMake presets set SPMATH, so no shipped config changes - this closes the integrator Makefile gap. WOLFBOOT_DICE_HW stays verify-only (requires WOLFCRYPT_TZ_PSA). --- include/user_settings.h | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/include/user_settings.h b/include/user_settings.h index 1234d7f7..16291235 100644 --- a/include/user_settings.h +++ b/include/user_settings.h @@ -167,18 +167,17 @@ extern int tolower(int c); #ifdef USE_FAST_MATH /* WC_NO_HARDEN suits verify-only builds, which do public-key - * operations only. Software DICE (WOLFCRYPT_TZ_PSA without - * WOLFBOOT_DICE_HW) signs the attestation claims with the private - * IAK, so it is excluded; hardware DICE keeps signing in the crypto + * operations only. Secure-mode worlds (TZ_PSA/PKCS11/FWTPM/WOLFHSM) + * process private keys in software, so they keep the timing- + * resistant TFM path; hardware DICE keeps signing in the crypto * engine and stays verify-only. */ -# if !defined(WOLFCRYPT_TZ_PSA) || defined(WOLFBOOT_DICE_HW) -# define WC_NO_HARDEN +# if defined(WOLFCRYPT_SECURE_MODE) && !defined(WOLFBOOT_DICE_HW) +# define TFM_TIMING_RESISTANT # else /* tfm.c never tests WC_NO_HARDEN, so dropping it alone changes * no code and only un-silences an advisory that -Werror turns - * into a build failure. TFM_TIMING_RESISTANT is what makes - * tfm.c constant time. */ -# define TFM_TIMING_RESISTANT + * into a build failure. */ +# define WC_NO_HARDEN # endif #endif From c1a7d89b2d898d5a824a12e536ab76de1b80a7d2 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 15 Sep 2026 18:04:21 +0200 Subject: [PATCH 17/49] unit-tests: mock wc_FreeRng for keygen_die keygen_die() (F-9767) calls wc_FreeRng(); the test mocks keygen.c's wolfCrypt dependencies one by one and was missing this one, breaking the unit_tests and test_external_libs jobs. --- tools/unit-tests/unit-keygen-xmss-params.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tools/unit-tests/unit-keygen-xmss-params.c b/tools/unit-tests/unit-keygen-xmss-params.c index b8e9a45f..0fe00fb0 100644 --- a/tools/unit-tests/unit-keygen-xmss-params.c +++ b/tools/unit-tests/unit-keygen-xmss-params.c @@ -22,8 +22,10 @@ static void mock_exit(int code); #define wc_XmssKey_ExportPubRaw mock_wc_XmssKey_ExportPubRaw #define wc_XmssKey_Free mock_wc_XmssKey_Free #define wc_ForceZero mock_wc_ForceZero +#define wc_FreeRng mock_wc_FreeRng #include "../keytools/keygen.c" #undef wc_ForceZero +#undef wc_FreeRng #undef wc_XmssKey_Free #undef wc_XmssKey_ExportPubRaw #undef wc_XmssKey_GetPrivLen @@ -117,6 +119,12 @@ void mock_wc_ForceZero(void *mem, size_t len) (void)len; } +int mock_wc_FreeRng(WC_RNG *rng) +{ + (void)rng; + return 0; +} + static void setup(void) { mock_xmss_param = NULL; From dcc6c64bd7063b434317204d09a95fc6ab47f490 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 15 Sep 2026 18:04:21 +0200 Subject: [PATCH 18/49] unit-tests: pin -std=gnu17 for host test builds GCC 14+ defaults to gnu23, where glibc string.h defines memchr and memcpy as _Generic macros that clash with wolfBoot's own declarations in tests that #include a .c file (unit-string). gnu17 matches the CI toolchain default. --- tools/unit-tests/Makefile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index 22a411d8..d6360938 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -41,6 +41,10 @@ CFLAGS+=-ftest-coverage CFLAGS+=--coverage CFLAGS+=-DUNIT_TEST_COVERAGE CFLAGS+=-DUNIT_TEST -DWOLFSSL_USER_SETTINGS +# Pin the standard: GCC 14+ defaults to gnu23, where glibc string.h defines +# memchr/memcpy as _Generic macros that clash with wolfBoot's own declarations +# in tests that #include a .c file. gnu17 matches the CI toolchain default. +CFLAGS+=-std=gnu17 LDFLAGS+=-fprofile-arcs LDFLAGS+=-ftest-coverage From 9f456bf86454935237efd641f5e16b3087a325f8 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 16 Sep 2026 12:14:13 +0200 Subject: [PATCH 19/49] F-9252: fix MPU system-control range end address Comment said 0xE0000000:0xEFFFFFF (255M) but the system-control region is 256M, ending at 0xEFFFFFFF. --- src/boot_arm.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/boot_arm.c b/src/boot_arm.c index d2eb1bc8..f59c76c2 100644 --- a/src/boot_arm.c +++ b/src/boot_arm.c @@ -173,7 +173,7 @@ static void mpu_init(void) mpu_setattr(6, MPUSIZE_1G | MPU_RASR_ENABLE | MPU_RASR_ATTR_S | MPU_RASR_ATTR_B | MPU_RASR_ATTR_AP_PRW_UNO | MPU_RASR_ATTR_XN); - /* System control 0xE0000000:0xEFFFFFF */ + /* System control 0xE0000000:0xEFFFFFFF (256M) */ mpu_setaddr(7, 0xE0000000); mpu_setattr(7, MPUSIZE_256M | MPU_RASR_ENABLE | MPU_RASR_ATTR_S | MPU_RASR_ATTR_B | MPU_RASR_ATTR_AP_PRW_UNO | MPU_RASR_ATTR_XN); From 09f0c9a5a394cc00f5a37888a160fdcac37ed7cc Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 16 Sep 2026 12:14:13 +0200 Subject: [PATCH 20/49] F-9253: disk_open accepts GPT or MBR, not GPT-only The @return contract said 'no valid GPT partition table' but disk_open falls back to parsing an MBR when no protective-MBR GPT is present. --- src/disk.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/disk.c b/src/disk.c index 53dc9e8d..79114a6c 100644 --- a/src/disk.c +++ b/src/disk.c @@ -117,7 +117,8 @@ static int disk_open_mbr(struct disk_drive *drive, const uint8_t *mbr_sector) * @param[in] drv The drive number to open (0 to `MAX_DISKS - 1`). * * @return The number of partitions found and initialized on success, or -1 if - * the drive cannot be opened or no valid GPT partition table is found. + * the drive cannot be opened or no valid partition table (GPT or MBR) is + * found. */ int disk_open(int drv) { From a2e623818399adc3f3c12abe2497962369dbfc11 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 16 Sep 2026 12:14:13 +0200 Subject: [PATCH 21/49] F-9254: XMSS import error comment named LMS params The failing call is wc_XmssKey_ImportPubRaw; the error comment referenced LMS parameters. --- src/image.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/image.c b/src/image.c index 1b97f22a..a028f2dc 100644 --- a/src/image.c +++ b/src/image.c @@ -776,7 +776,7 @@ static void wolfBoot_verify_signature_xmss(uint8_t key_slot, /* Set the public key. */ ret = wc_XmssKey_ImportPubRaw(&xmss, pubkey, KEYSTORE_PUBKEY_SIZE); if (ret != 0) { - /* Something is wrong with the pub key or LMS parameters. */ + /* Something is wrong with the pub key or XMSS parameters. */ wolfBoot_printf("error: wc_XmssKey_ImportPubRaw" \ " returned %d\n", ret); return; From 36c57846f51bb9e5b94dd92f992dd7f5248d5507 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 16 Sep 2026 12:14:13 +0200 Subject: [PATCH 22/49] F-9256: rejected ELF does not rewrite the entry point The rationale that a rejected ELF leaves load_address rewritten no longer matches the code. The skip flag is set on each stage's success path. --- src/update_ram.c | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/update_ram.c b/src/update_ram.c index b1b23702..02b84608 100644 --- a/src/update_ram.c +++ b/src/update_ram.c @@ -781,11 +781,10 @@ backup_on_failure: #endif /* MMU */ #ifdef WOLFBOOT_UBOOT_LEGACY - /* Enter the uImage at ih_ep. Skipped if a later stage (ELF/FIT) re-derived - * the load address, since that stage provides its own entry point. The - * flag is tracked explicitly rather than by comparing load_address: - * elf_load_image_mmu() publishes its entry point before it finishes - * validating, so a rejected ELF also leaves load_address rewritten. */ + /* Enter the uImage at ih_ep. Skipped if a later stage (ELF/FIT) succeeded + * and re-derived the load address, since that stage provides its own + * entry point. Tracked with an explicit flag set on each stage's success + * path rather than by comparing load_address. */ if ((uboot_entry != NULL) && !stage_entry_override) { load_address = uboot_entry; } From 7a6e450ec6018014c8b19d35c715ff80b9067b97 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 16 Sep 2026 12:14:13 +0200 Subject: [PATCH 23/49] F-9257: DDR training retry: WRCALIB is the gate The reliability gate is WRCALIB, not PHY_TRAINING_STATUS. MTC is a secondary gate skipped when TIP completes full training, and the retry budget is 3 inner x 6 outer (up to 18 attempts), not 5. --- hal/mpfs250_ddr.c | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/hal/mpfs250_ddr.c b/hal/mpfs250_ddr.c index ba7b6607..551c82f2 100644 --- a/hal/mpfs250_ddr.c +++ b/hal/mpfs250_ddr.c @@ -3125,21 +3125,23 @@ int mpfs_ddr_init(unsigned int outer_retry) DDRPHY_REG(PHY_TIP_CFG_PARAMS) = LIBERO_SETTING_TIP_CFG_PARAMS; mb(); - /* Step 9: Run training + post-training + MTC sanity, with retry on - * MTC failure. + /* Step 9: Run training + post-training write calibration, with retry on + * calibration failure. * - * Why MTC is the retry trigger (not PHY_TRAINING_STATUS): when the - * manual ADDCMD training picks a marginal phase/dly that doesn't - * resolve into a usable DRAM alignment, train_stat sticks at 0x1 - * (BCLK_SCLK only). But TIP keeps spinning in the background and - * eventually flips the WRLVL/RDGATE/DQ_DQS bits to read 0x1D, even - * though the alignment is bogus. An outer retry keyed on - * PHY_TRAINING_STATUS sees that bogus 0x1D and stops. MTC actually - * exercises the DDR controller -- it times out unambiguously when - * training was bad, and is the reliable signal. + * The reliability gate is WRCALIB (the per-lane write-cal sweep must + * calibrate all lanes), not PHY_TRAINING_STATUS: when the manual ADDCMD + * training picks a marginal phase/dly that doesn't resolve into a usable + * DRAM alignment, TIP's train_stat self-report can still read "complete" + * even though the write path is bad. Gating on train_stat alone let bad + * boots through and the 19 MB load then hard-failed every block. * - * Empirical baseline: ~30% per-attempt training failure rate -> 5 - * retries gives ~99.7% cumulative success rate. + * MTC sanity is a secondary gate, run only when TIP did not report full + * training (0x1C). When TIP does complete full training, MTC is skipped: + * the MTC engine has a separate DDRC-internal access issue and would just + * burn all retries and end in a WDT reset. + * + * Empirical baseline: ~30% per-attempt training failure rate; 3 inner x + * 6 outer retries (up to 18 attempts) covers it with margin. */ { uint32_t train_retry = 0; From 96cd60d28db75f406e5ad3cc3c39c1ed871a3af6 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 16 Sep 2026 12:14:13 +0200 Subject: [PATCH 24/49] F-9258: SPI cont keeps CS low (asserted) cont!=0 holds chip-select asserted (low) after the transfer; the comment said 'not let CS go low'. --- hal/nxp_ls1028a.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hal/nxp_ls1028a.c b/hal/nxp_ls1028a.c index 190fd49a..e493fd63 100644 --- a/hal/nxp_ls1028a.c +++ b/hal/nxp_ls1028a.c @@ -134,7 +134,7 @@ static void spi_push_tx(unsigned int sel, unsigned int pcs, unsigned char data, | SPI_PUSHR_PCS(pcs) | data; } -/* Perform a SPI transaction. Set cont!=0 to not let CS go low after this*/ +/* Perform a SPI transaction. Set cont!=0 to keep CS low (asserted) after this */ static void spi_transaction(unsigned int sel, unsigned int pcs, const unsigned char *out, unsigned char *in, unsigned int size, int cont) From d170977f508b4fe15cce06b260e13534da0251f9 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 16 Sep 2026 12:14:13 +0200 Subject: [PATCH 25/49] F-9259: T2080 handoff sets TCR to 0 wolfBoot clears TCR to 0 (matching CW U-Boot pre-bootm); the comment described the old 0x04000000 value. --- hal/nxp_t2080.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hal/nxp_t2080.c b/hal/nxp_t2080.c index d93716dd..d39f6a6b 100644 --- a/hal/nxp_t2080.c +++ b/hal/nxp_t2080.c @@ -1703,8 +1703,8 @@ void hal_prepare_boot(void) * profile when chasing VxWorks 7 64-bit silent boot: * - DUART1 MCR = 3 (DTR+RTS asserted; U-Boot sets this, our driver * leaves it at the post-reset 0) - * - TCR = 0x04000000 (matches U-Boot's leftover; wolfBoot was clearing - * it; VxWorks 7 BSP early code may inherit) */ + * - TCR = 0 (matches CW U-Boot's pre-bootm value; a nonzero WRC would let + * the watchdog fire silently after VxWorks starts) */ void RAMFUNCTION hal_flash_cache_disable_pre_os(void) { hal_flash_cache_disable(); From b9f5cfdefeb20490b5cc8462f1018c3362459392 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 16 Sep 2026 12:14:13 +0200 Subject: [PATCH 26/49] F-9260: S32K watchdog timeout is ~131s, not ~2s WDOG_TOVAL=0xFFFF with a 1:256 prescaler at 128kHz LPO gives ~131 seconds, not ~2 seconds. --- hal/s32k1xx.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hal/s32k1xx.c b/hal/s32k1xx.c index 74923582..b5578f9f 100644 --- a/hal/s32k1xx.c +++ b/hal/s32k1xx.c @@ -519,8 +519,9 @@ void hal_prepare_boot(void) WDOG_CNT = WDOG_CNT_UNLOCK; while (!(WDOG_CS & WDOG_CS_ULK)) {} - /* Enable watchdog with ~2 second timeout (256k ticks at 128kHz LPO) - * Application should either service or reconfigure the watchdog + /* Enable watchdog with ~131 second timeout (65535 ticks at 128kHz + * LPO, 1:256 prescaler). Application should either service or + * reconfigure the watchdog */ WDOG_TOVAL = 0xFFFF; /* Max timeout ~512ms without prescaler */ WDOG_CS = WDOG_CS_EN | WDOG_CS_UPDATE | WDOG_CS_CMD32EN | From c527ac7ee1bd64b5ea74a9168df4f4322d6d79cc Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 16 Sep 2026 12:14:13 +0200 Subject: [PATCH 27/49] F-9261: sector-size check allows equality The #if fires on WOLFBOOT_SECTOR_SIZE < IMAGE_HEADER_SIZE, so equal is accepted; the comment and #error said 'larger than'. --- include/image.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/image.h b/include/image.h index f5844498..ab5410f3 100644 --- a/include/image.h +++ b/include/image.h @@ -133,10 +133,10 @@ extern "C" { #define wolfBoot_verify_signature_primary wolfBoot_verify_signature_tpm #endif -/* Validate sector size is larger than image header size */ +/* Validate sector size is at least as large as the image header size */ #if defined(WOLFBOOT_SECTOR_SIZE) && defined(IMAGE_HEADER_SIZE) && \ (WOLFBOOT_SECTOR_SIZE < IMAGE_HEADER_SIZE) -#error WOLFBOOT_SECTOR_SIZE must be larger than IMAGE_HEADER_SIZE +#error WOLFBOOT_SECTOR_SIZE must be at least as large as IMAGE_HEADER_SIZE #endif From c58a6905f0e1e16bc70220195f7194dfb1e1df92 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 16 Sep 2026 12:14:13 +0200 Subject: [PATCH 28/49] F-9723: non-MMU handoff clears r0-r3, not all GPRs The code clears only r0-r3 for the handoff; the comment said all GPRs cleared. --- src/boot_arm32.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/boot_arm32.c b/src/boot_arm32.c index 1ab63868..381b750c 100644 --- a/src/boot_arm32.c +++ b/src/boot_arm32.c @@ -72,7 +72,7 @@ void RAMFUNCTION do_boot(const uint32_t *app_offset) * removes the need for a separate LINUX_PAYLOAD switch per target. * * Without MMU there is no DTB to pass, so we fall back to a minimal - * handoff (all GPRs cleared) used by targets like sama5d3. */ + * handoff (r0-r3 cleared) used by targets like sama5d3. */ #ifdef MMU register const uint32_t *dts_in = dts_offset; asm volatile ( From ae2145a3800cfe8ff28e11155953eca7cd3bd861 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 16 Sep 2026 12:14:13 +0200 Subject: [PATCH 29/49] F-9724: partial-flash-write uses bitwise NOT The formula is data | ~current_data (bitwise); the comment used the logical '!' --- hal/pic32c.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hal/pic32c.c b/hal/pic32c.c index aa31097d..1ec3f932 100644 --- a/hal/pic32c.c +++ b/hal/pic32c.c @@ -223,7 +223,7 @@ int pic32_flash_write(uint32_t address, const uint8_t *data, int len) _addr = pic32_addr_dqword_align(address); /* Setup an aligned buffer with the following rules: * - For addresses outside the writing range: 0xFF (no change) - * - For addresses inside the writing range: data | !current_data + * - For addresses inside the writing range: data | ~current_data * * This approach ensures we only flip bits from 1 to 0 when writing * without an erase operation. When the address is aligned and length From 43a0e59f37d0777909e6a6683191e2ac836b317d Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 16 Sep 2026 12:14:13 +0200 Subject: [PATCH 30/49] F-12890: change_stack_and_invoke has no callback arg The function takes (new_stack, other_func) only; the doc described a nonexistent 'ptr' parameter. --- src/boot_x86_fsp.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/boot_x86_fsp.c b/src/boot_x86_fsp.c index 85edaf17..5b4596f1 100644 --- a/src/boot_x86_fsp.c +++ b/src/boot_x86_fsp.c @@ -147,11 +147,10 @@ static int get_top_address(uint64_t *top, struct efi_hob *hoblist) * \brief Change the stack and invoke a function with the new stack. * * This function changes the stack to the specified 'new_stack' value and then - * calls the function pointed to by 'other_func', passing the 'ptr' parameter as an argument. + * calls the function pointed to by 'other_func'. * * \param new_stack The new stack address. * \param other_func Pointer to the function to be invoked with the new stack. - * \param ptr Pointer to the parameter to be passed to the invoked function. */ static void change_stack_and_invoke(uint32_t new_stack, void (*other_func)(void)) From e32ea8a659ac00058083521375b93a57e8b36fda Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 16 Sep 2026 12:14:13 +0200 Subject: [PATCH 31/49] F-12891: wolfBoot_tpm2_extend PCR range is 0-23 PCR 24 is not a valid extend target; the doc said 0-24. --- src/tpm.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tpm.c b/src/tpm.c index 3c9fc21e..e240946e 100644 --- a/src/tpm.c +++ b/src/tpm.c @@ -336,7 +336,7 @@ static int self_sha384(uint8_t *hash) * TPM2_PCR_Extend. Optionally, if DEBUG_WOLFTPM or WOLFBOOT_DEBUG_TPM defined, * prints debug info. * - * @param[in] pcrIndex The PCR Index (0-24 is valid range). + * @param[in] pcrIndex The PCR Index (0-23 is valid range). * @param[in] hash Pointer to the hash value to extend into the PCR. * @param[in] line Line number where the function is called (for debugging). * @return 0 on success, an error code on failure. From c8b799bda9a898e97eb0b3ba5fd0e079c9ecaf1c Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 16 Sep 2026 12:15:00 +0200 Subject: [PATCH 32/49] F-11013: SDHCI no-base-clock path returns 0 as an error The no-base-clock path disables the clock and returns 0 (error), unlike the already-set path above which returns last_clock_khz. The old NOTE claimed callers could not tell the two apart. --- src/sdhci.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sdhci.c b/src/sdhci.c index af046f24..f2977c84 100644 --- a/src/sdhci.c +++ b/src/sdhci.c @@ -491,9 +491,9 @@ static uint32_t sdhci_set_clock(uint32_t clock_khz) base_clk_khz = sdhci_platform_set_clock(clock_khz, base_clk_khz); if (base_clk_khz == 0) { /* No usable base clock. The SD clock was already disabled above, so - * the controller is left idle. NOTE: 0 is also what the "clock already - * set" path above returns, so callers cannot currently tell these - * apart - see the DEBUG_SDHCI log for which one happened. */ + * the controller is left idle. This path returns 0 (error), unlike + * the "clock already set" path above which returns last_clock_khz, + * so a 0 return is unambiguously an error for callers. */ #ifdef DEBUG_SDHCI wolfBoot_printf("sdhci_set_clock: no usable base clock " "(CAPS and platform hook both 0)\n"); From bed6df776d5457aada650d09ea7d1b26a03b4c30 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 16 Sep 2026 12:15:08 +0200 Subject: [PATCH 33/49] F-9255: fix eMMC ready-bit polarity in comment The code treats OCR bit 31 set as ready (response & MMC_OCR_BUSY_BIT); the comment said busy bit cleared equals ready. --- src/sdhci.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sdhci.c b/src/sdhci.c index f2977c84..0bb7da8e 100644 --- a/src/sdhci.c +++ b/src/sdhci.c @@ -1170,7 +1170,7 @@ static int emmc_send_op_cond(uint32_t ocr_arg, uint32_t *ocr_reg) response = SDHCI_REG(SDHCI_SRS04); - /* Check if device is ready (busy bit cleared = ready) */ + /* Check if device is ready (OCR bit 31 set = ready) */ if (response & MMC_OCR_BUSY_BIT) { /* Device is ready */ if (ocr_reg != NULL) { From 180647eb1c84b47b136a21e124cd29c25bfde5a9 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 16 Sep 2026 12:15:15 +0200 Subject: [PATCH 34/49] F-12893: drop invented @return from set_update_sector_flags doc set_update_sector_flags is void; the doc claimed a 0/-1 status return. --- src/libwolfboot.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libwolfboot.c b/src/libwolfboot.c index 932677a1..8a5e9973 100644 --- a/src/libwolfboot.c +++ b/src/libwolfboot.c @@ -692,7 +692,6 @@ static void RAMFUNCTION set_partition_state(uint8_t part, uint8_t val) * * @param[in] pos Update sector position. * @param[in] val New flags value to set. - * @return 0 on success, -1 on failure. */ static void RAMFUNCTION set_update_sector_flags(uint32_t pos, uint8_t val) { From 17d6cc2604119a4ac61580abc1c978bd7b9b178d Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 16 Sep 2026 12:15:23 +0200 Subject: [PATCH 35/49] F-12892: wolfBoot_update_trigger acts on the update partition The function sets the update partition state to IMG_STATE_UPDATING and erases the update partition last sector; the doc said boot partition. --- src/libwolfboot.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libwolfboot.c b/src/libwolfboot.c index 8a5e9973..a1a66e3b 100644 --- a/src/libwolfboot.c +++ b/src/libwolfboot.c @@ -865,8 +865,8 @@ void RAMFUNCTION wolfBoot_erase_partition(uint8_t part) /** * @brief Update trigger function. * - * This function updates the boot partition state to "IMG_STATE_UPDATING". - * If the FLAGS_HOME macro is defined, it erases the last sector of the boot + * This function sets the update partition state to "IMG_STATE_UPDATING". + * If the FLAGS_HOME macro is defined, it erases the last sector of the update * partition before updating the partition state. It also checks FLAGS_UPDATE_EXT * and calls the appropriate flash unlock and lock functions before * updating the partition state. From 44908c3e5d361857f51f4dd752c5358eb544237e Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 16 Sep 2026 12:15:31 +0200 Subject: [PATCH 36/49] F-12076: ext_flash_decrypt_read is not AES-only Decryption is ChaCha20, AES-CTR, or PKCS#11 per build config; the doc implied AES only. --- src/libwolfboot.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libwolfboot.c b/src/libwolfboot.c index a1a66e3b..5dc94705 100644 --- a/src/libwolfboot.c +++ b/src/libwolfboot.c @@ -2835,8 +2835,8 @@ exit: * @brief Read and decrypt data from an external flash. * * This function reads the encrypted data from the external flash, - * decrypts it using the AES decryption algorithm, and stores the decrypted data - * in the provided buffer. + * decrypts it using the configured decryption algorithm (ChaCha20, AES-CTR, + * or PKCS#11), and stores the decrypted data in the provided buffer. * @param address The address in the external flash to read the encrypted data from. * @param data Pointer to the buffer to store the decrypted data. From b7857b99f974272ad3118216b81763df5530fc3c Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 16 Sep 2026 12:15:37 +0200 Subject: [PATCH 37/49] F-12077: wolfBoot_ram_decrypt is not AES-only Decryption is ChaCha20, AES-CTR, or PKCS#11 per build config; the doc implied AES only. --- src/libwolfboot.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libwolfboot.c b/src/libwolfboot.c index 5dc94705..05162cbf 100644 --- a/src/libwolfboot.c +++ b/src/libwolfboot.c @@ -2959,7 +2959,8 @@ typedef char wolfBoot_ramboot_blockalign_check[ /** * @brief Decrypt data from RAM. * - * This function decrypts data from the RAM using the AES decryption algorithm. + * This function decrypts data from the RAM using the configured decryption + * algorithm (ChaCha20, AES-CTR, or PKCS#11). * * @param src Pointer to the source buffer containing the encrypted data. * @param dst Pointer to the destination buffer to store the decrypted data. From cc7fec1ea20e9cd3463fbb763c89a66b14f72a13 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 16 Sep 2026 14:30:26 +0200 Subject: [PATCH 38/49] PR 892: anti-rollback for CUSTOM_PARTITION_TRAILER targets Add HAVE_PARTITION_TRAILERS macro (set when WOLFBOOT_FIXED_PARTITIONS or CUSTOM_PARTITION_TRAILER is defined) and use it to gate the TESTING state transition in update_ram.c. Previously the gate was WOLFBOOT_FIXED_PARTITIONS only, so targets using custom partition trailers never transitioned from UPDATING to TESTING, allowing rollback to older versions. Add unit-update-ram-custom-trailer test covering the CUSTOM_PARTITION_TRAILER path with mocked get/set_trailer_at callbacks. --- include/wolfboot/wolfboot.h | 9 + src/update_ram.c | 4 +- tools/unit-tests/Makefile | 15 +- .../unit-update-ram-custom-trailer.c | 331 ++++++++++++++++++ 4 files changed, 356 insertions(+), 3 deletions(-) create mode 100644 tools/unit-tests/unit-update-ram-custom-trailer.c diff --git a/include/wolfboot/wolfboot.h b/include/wolfboot/wolfboot.h index 1b947395..d86812d6 100644 --- a/include/wolfboot/wolfboot.h +++ b/include/wolfboot/wolfboot.h @@ -43,6 +43,15 @@ extern "C" { #include "wolfboot/version.h" #include "wolfboot/wc_secure.h" +/* Partition trailers (magic + state flags) are persisted in flash only when + * the target has fixed partitions or supplies a custom trailer backend. + * Without either, get/set_trailer_at() are no-op stubs and the + * wolfBoot_{get,set}_partition_state() API is absent, so the fallback + * decision must be made on version + image validity alone. */ +#if defined(WOLFBOOT_FIXED_PARTITIONS) || defined(CUSTOM_PARTITION_TRAILER) + #define HAVE_PARTITION_TRAILERS 1 +#endif + #ifndef RAMFUNCTION # if defined(__WOLFBOOT) && defined(RAM_CODE) diff --git a/src/update_ram.c b/src/update_ram.c index 02b84608..d5a541df 100644 --- a/src/update_ram.c +++ b/src/update_ram.c @@ -286,7 +286,7 @@ void RAMFUNCTION wolfBoot_start(void) #endif uint32_t *load_address = NULL; uint32_t *source_address = NULL; -#ifdef WOLFBOOT_FIXED_PARTITIONS +#ifdef HAVE_PARTITION_TRAILERS uint8_t p_state; #endif #if defined(MMU) || defined(WOLFBOOT_FDT) @@ -452,7 +452,7 @@ backup_on_failure: /* First time we boot this update, set to TESTING to await * confirmation from the system */ -#ifdef WOLFBOOT_FIXED_PARTITIONS +#ifdef HAVE_PARTITION_TRAILERS if ((wolfBoot_get_partition_state(active, &p_state) == 0) && (p_state == IMG_STATE_UPDATING)) { diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index d6360938..41ed6946 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -66,7 +66,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-nofixed-noramboot 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-ram-custom-trailer 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 \ @@ -351,6 +351,16 @@ unit-update-ram-nofixed-noramboot:CFLAGS+=-DMOCK_PARTITIONS -DWOLFBOOT_NO_SIGN \ -DWOLFBOOT_NO_PARTITIONS -DUNIT_TEST_NO_FIXED_PARTITIONS \ -DWOLFBOOT_ORIGIN=MOCK_ADDRESS_BOOT \ -DBOOTLOADER_PARTITION_SIZE=WOLFBOOT_PARTITION_SIZE +# CUSTOM_PARTITION_TRAILER: covers the HAVE_PARTITION_TRAILERS path where +# partition state is managed via externally-defined get/set_trailer_at. +unit-update-ram-custom-trailer: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_FIXED_PARTITIONS \ + -DCUSTOM_PARTITION_TRAILER \ + -DWOLFBOOT_RAMBOOT_MAX_SIZE=WOLFBOOT_PARTITION_SIZE \ + -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 \ @@ -933,6 +943,9 @@ unit-update-ram-nofixed-noramboot: ../../include/target.h unit-update-ram-nofixe 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) +unit-update-ram-custom-trailer: ../../include/target.h unit-update-ram-custom-trailer.c + gcc -o $@ unit-update-ram-custom-trailer.c ../../src/image.c $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha256.c $(CFLAGS) $(LDFLAGS) + unit-update-flash-hwswap: ../../include/target.h unit-update-flash-hwswap.c gcc -o $@ unit-update-flash-hwswap.c ../../src/image.c $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha256.c $(CFLAGS) $(LDFLAGS) diff --git a/tools/unit-tests/unit-update-ram-custom-trailer.c b/tools/unit-tests/unit-update-ram-custom-trailer.c new file mode 100644 index 00000000..0b31d66d --- /dev/null +++ b/tools/unit-tests/unit-update-ram-custom-trailer.c @@ -0,0 +1,331 @@ +/* unit-update-ram-custom-trailer.c + * + * Tests update_ram.c with CUSTOM_PARTITION_TRAILER (custom callbacks) and + * WOLFBOOT_FIXED_PARTITIONS. Covers the HAVE_PARTITION_TRAILERS path where + * partition state is managed via externally-defined get/set_trailer_at. + */ +#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" + +static __thread unsigned char + wolfboot_ram[WOLFBOOT_RAMBOOT_MAX_SIZE + IMAGE_HEADER_SIZE]; + +#define WOLFBOOT_LOAD_ADDRESS (((uintptr_t)wolfboot_ram) + IMAGE_HEADER_SIZE) +#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" + +/* Custom partition trailer state (mocked) + * Layout: [state][magic(4 bytes)] + * get_trailer_at(part, 0) -> magic (32-bit, base[1..4]) + * get_trailer_at(part, 1) -> state (8-bit, base[0]) + */ +static uint8_t mock_trailer_boot[5]; +static uint8_t mock_trailer_update[5]; + +uint8_t* get_trailer_at(uint8_t part, uint32_t at) +{ + uint8_t *base = (part == PART_BOOT) ? mock_trailer_boot : mock_trailer_update; + if (at == 0) + return &base[1]; /* magic at base[1..4] */ + return &base[at - 1]; /* state at base[0], etc. */ +} + +void set_trailer_at(uint8_t part, uint32_t at, uint8_t val) +{ + uint8_t *base = (part == PART_BOOT) ? mock_trailer_boot : mock_trailer_update; + if (at == 0) + base[1] = val; /* magic byte 0 */ + else + base[at - 1] = val; +} + +void set_partition_magic(uint8_t part) +{ + uint8_t *base = (part == PART_BOOT) ? mock_trailer_boot : mock_trailer_update; + /* WOLFBOOT_MAGIC_TRAIL = 0x544F4F42 on LE: bytes are 'B','O','O','T' */ + base[1] = 'B'; + base[2] = 'O'; + base[3] = 'O'; + base[4] = 'T'; + (void)part; +} + +#define wolfBoot_dualboot_candidate wolfBoot_dualboot_candidate_impl +#include "libwolfboot.c" +#undef wolfBoot_dualboot_candidate + +static int dualboot_candidate_calls; + +int wolfBoot_dualboot_candidate(void) +{ + dualboot_candidate_calls++; + ck_assert_msg(dualboot_candidate_calls == 1, + "wolfBoot_dualboot_candidate() called %d times", + dualboot_candidate_calls); + return wolfBoot_dualboot_candidate_impl(); +} + +#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) +{ + 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_calls = 0; + mock_flash_protect_called = 0; + mock_flash_protect_addr = 0; + mock_flash_protect_len = 0; + memset(mock_trailer_boot, 0, sizeof(mock_trailer_boot)); + memset(mock_trailer_update, 0, sizeof(mock_trailer_update)); +} + +static void prepare_flash(void) +{ + int ret; + + ret = mmap_file("/tmp/wolfboot-unit-ext-file-custom-trailer.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-custom-trailer.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; + uint8_t *base = (uint8_t *)WOLFBOOT_PARTITION_BOOT_ADDRESS; + int ret; + 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; +} + +/* Test 1: Update partition in UPDATING state transitions to TESTING after boot */ +START_TEST(test_custom_trailer_updating_sets_testing) +{ + uint8_t state; + + 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); + + /* Set the update partition to UPDATING state via custom trailer */ + set_partition_magic(PART_UPDATE); + mock_trailer_update[0] = IMG_STATE_UPDATING; /* state at base[0] */ + + wolfBoot_start(); + + /* After boot, the update partition should be in TESTING state */ + ck_assert_int_eq(wolfBoot_staged_ok, 1); + ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_int_eq(wolfBoot_get_partition_state(PART_UPDATE, &state), 0); + ck_assert_int_eq(state, IMG_STATE_TESTING); + + cleanup_flash(); +} +END_TEST + +/* Test 2: Invalid update falls back to boot partition */ +START_TEST(test_custom_trailer_invalid_update_falls_back_to_boot) +{ + uint8_t bad_digest[SHA256_DIGEST_SIZE]; + + 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); + + 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(); + + /* Falls back to boot partition (version 1) */ + ck_assert_int_eq(wolfBoot_staged_ok, 1); + ck_assert_int_eq(wolfBoot_panicked, 0); + + cleanup_flash(); +} +END_TEST + +/* Test 3: Newer update is preferred over boot */ +START_TEST(test_custom_trailer_newer_update_prefers_update) +{ + int candidate; + + 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); + + candidate = wolfBoot_dualboot_candidate_impl(); + + ck_assert_int_eq(candidate, PART_UPDATE); + cleanup_flash(); +} +END_TEST + +static Suite *wolfboot_suite(void) +{ + Suite *s = suite_create("wolfboot-update-ram-custom-trailer"); + TCase *tc = tcase_create("custom_trailer"); + + tcase_add_test(tc, test_custom_trailer_updating_sets_testing); + tcase_add_test(tc, test_custom_trailer_invalid_update_falls_back_to_boot); + tcase_add_test(tc, test_custom_trailer_newer_update_prefers_update); + tcase_set_timeout(tc, 5); + suite_add_tcase(s, tc); + + 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 == 0) ? EXIT_SUCCESS : EXIT_FAILURE; +} From 66be5eb520d93f36b6742dad75a996032fec791b Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 16 Sep 2026 14:32:09 +0200 Subject: [PATCH 39/49] PR 892: P1021 NAND status bits + test coverage for F-12883/F-12922 - hal/nxp_p1021.c: fix JEDEC status bit polarity (DQ0 clear = fail, DQ1 clear = protected; was checking set bits) - hal/s32k1xx.c: clarify watchdog timeout comment (65535 ticks at 128kHz LPO = ~512ms unprescaled, ~131s with 1:256 prescaler) - unit-keygen-xmss-params.c: assert wc_ForceZero + wc_FreeRng called after keygen (F-12883) - unit-mock-flash.c: track largest ext_flash_read source addr/len - unit-p1021-erase-advance.c: add test_erase_stops_on_status_fail (cmd succeeds but NAND reports DQ0 clear) - unit-update-ram-noramboot.c: verify fallback loads from UPDATE partition, not oversize BOOT (F-12922) --- hal/nxp_p1021.c | 23 ++++++++------ hal/s32k1xx.c | 12 ++++---- tools/unit-tests/unit-keygen-xmss-params.c | 11 +++++++ tools/unit-tests/unit-mock-flash.c | 9 ++++++ tools/unit-tests/unit-p1021-erase-advance.c | 32 +++++++++++++++++--- tools/unit-tests/unit-p1021-fcm-bytes.c | 2 ++ tools/unit-tests/unit-update-ram-noramboot.c | 8 ++++- 7 files changed, 77 insertions(+), 20 deletions(-) diff --git a/hal/nxp_p1021.c b/hal/nxp_p1021.c index 8052d65a..e5e52cda 100644 --- a/hal/nxp_p1021.c +++ b/hal/nxp_p1021.c @@ -336,9 +336,11 @@ enum elbc_amask_sizes { #define NAND_CMD_READSTART 0x30 /* Extended command for large page devices */ -/* NAND device status byte (JEDEC), returned in MDR by the RS/RSW ops */ -#define NAND_STATUS_WPS (1 << 1) /* write protect status */ -#define NAND_STATUS_FAIL (1 << 3) /* P: program/erase fail */ +/* NAND device status byte (JEDEC), returned in MDR by the RS/RSW ops: + * DQ0 clear = program/erase failed, DQ1 clear = write protected. A good + * status has both bits set. */ +#define NAND_STATUS_FAIL (1 << 0) /* DQ0: 0 = program/erase fail */ +#define NAND_STATUS_WPS (1 << 1) /* DQ1: 0 = write protected */ /* DDR */ @@ -1709,9 +1711,11 @@ int ext_flash_write(uintptr_t address, const uint8_t *data, int len) wolfBoot_printf("write page %d, col %d, status %x\n", page, col, status); #endif - /* P (program fail) or WPS (write protect) set: the page did not - * program. Stop; retrying the same page fails the same way. */ - if (status & (NAND_STATUS_FAIL | NAND_STATUS_WPS)) { + /* DQ0 clear = program failed, DQ1 clear = write protected: the + * page did not program. Stop; retrying the same page fails the + * same way. */ + if ((status & (NAND_STATUS_FAIL | NAND_STATUS_WPS)) != + (NAND_STATUS_FAIL | NAND_STATUS_WPS)) { ret = -1; break; } @@ -1875,9 +1879,10 @@ int ext_flash_erase(uintptr_t address, int len) #ifdef DEBUG_EXT_FLASH wolfBoot_printf("erase page %d, status %x\n", page, status); #endif - /* P (erase fail) or WPS (write protect) set: the block did not - * erase. Stop; erasing the same block fails the same way. */ - if (status & (NAND_STATUS_FAIL | NAND_STATUS_WPS)) { + /* DQ0 clear = erase failed, DQ1 clear = write protected: the block + * did not erase. Stop; erasing the same block fails the same way. */ + if ((status & (NAND_STATUS_FAIL | NAND_STATUS_WPS)) != + (NAND_STATUS_FAIL | NAND_STATUS_WPS)) { ret = -1; break; } diff --git a/hal/s32k1xx.c b/hal/s32k1xx.c index b5578f9f..0e7f732c 100644 --- a/hal/s32k1xx.c +++ b/hal/s32k1xx.c @@ -519,13 +519,13 @@ void hal_prepare_boot(void) WDOG_CNT = WDOG_CNT_UNLOCK; while (!(WDOG_CS & WDOG_CS_ULK)) {} - /* Enable watchdog with ~131 second timeout (65535 ticks at 128kHz - * LPO, 1:256 prescaler). Application should either service or - * reconfigure the watchdog - */ - WDOG_TOVAL = 0xFFFF; /* Max timeout ~512ms without prescaler */ + /* Enable watchdog with 65535 ticks at 128kHz LPO: ~512ms + * without the prescaler, ~131 seconds with the 1:256 prescaler + * (WDOG_CS_PRES). Application should either service or + * reconfigure the watchdog. */ + WDOG_TOVAL = 0xFFFF; WDOG_CS = WDOG_CS_EN | WDOG_CS_UPDATE | WDOG_CS_CMD32EN | - WDOG_CS_CLK_LPO | WDOG_CS_PRES; /* With prescaler: ~131 sec */ + WDOG_CS_CLK_LPO | WDOG_CS_PRES; /* Wait for reconfiguration to complete */ while (!(WDOG_CS & WDOG_CS_RCS)) {} diff --git a/tools/unit-tests/unit-keygen-xmss-params.c b/tools/unit-tests/unit-keygen-xmss-params.c index 0fe00fb0..e40a6478 100644 --- a/tools/unit-tests/unit-keygen-xmss-params.c +++ b/tools/unit-tests/unit-keygen-xmss-params.c @@ -7,6 +7,8 @@ static const char *mock_xmss_param; static int mock_exit_code; static jmp_buf mock_exit_env; +static int mock_force_zero_count; +static int mock_free_rng_count; static void mock_exit(int code); @@ -117,11 +119,13 @@ void mock_wc_ForceZero(void *mem, size_t len) { (void)mem; (void)len; + mock_force_zero_count++; } int mock_wc_FreeRng(WC_RNG *rng) { (void)rng; + mock_free_rng_count++; return 0; } @@ -129,6 +133,8 @@ static void setup(void) { mock_xmss_param = NULL; mock_exit_code = 0; + mock_force_zero_count = 0; + mock_free_rng_count = 0; unsetenv("XMSS_PARAMS"); } @@ -148,6 +154,11 @@ static void run_keygen_xmss(void) ck_assert_int_eq(jumped, 1); ck_assert_int_eq(mock_exit_code, 1); + /* The RNG must be freed (wc_FreeRng) and zeroized (wc_ForceZero) after + * key generation (F-12883). keygen_die() frees+zeroizes the RNG; the + * key is also zeroized in the caller's cleanup. */ + ck_assert_int_ge(mock_force_zero_count, 1); + ck_assert_int_eq(mock_free_rng_count, 1); } START_TEST(test_keygen_xmss_uses_env_param_when_set) diff --git a/tools/unit-tests/unit-mock-flash.c b/tools/unit-tests/unit-mock-flash.c index c0d29618..476faf4e 100644 --- a/tools/unit-tests/unit-mock-flash.c +++ b/tools/unit-tests/unit-mock-flash.c @@ -351,6 +351,11 @@ int ext_flash_write(uintptr_t address, const uint8_t *data, int len) int mock_ext_flash_short_len = 0; int mock_ext_flash_short_bytes = 0; +/* Records the source address of the largest ext_flash_read() call (the + * image load to RAM), so a test can verify which partition was booted. */ +uintptr_t mock_max_read_addr = 0; +int mock_max_read_len = 0; + int ext_flash_read(uintptr_t address, uint8_t *data, int len) { int i; @@ -359,6 +364,10 @@ int ext_flash_read(uintptr_t address, uint8_t *data, int len) if (mock_ext_flash_short_len == len && mock_ext_flash_short_bytes > 0) ret = len - mock_ext_flash_short_bytes; + if (ret > mock_max_read_len) { + mock_max_read_len = ret; + mock_max_read_addr = address; + } for (i = 0; i < ret; i++) { data[i] = a[i]; } diff --git a/tools/unit-tests/unit-p1021-erase-advance.c b/tools/unit-tests/unit-p1021-erase-advance.c index 3d4b4c50..49c8772a 100644 --- a/tools/unit-tests/unit-p1021-erase-advance.c +++ b/tools/unit-tests/unit-p1021-erase-advance.c @@ -45,12 +45,19 @@ static int g_pages[MAX_TRACKED_PAGES]; static int g_page_calls; static int g_cmd_calls; static int g_cmd_ret; +static uint32_t g_status; -static void mock_reset(int cmd_ret) +/* JEDEC status byte: DQ0 set = program/erase ok, DQ1 set = not protected. + * 0x03 is a clean success; 0x02 is an erase/program failure (DQ0 clear). */ +#define STATUS_OK 0x03 +#define STATUS_ERASE_FAIL 0x02 + +static void mock_reset(int cmd_ret, uint32_t status) { g_page_calls = 0; g_cmd_calls = 0; g_cmd_ret = cmd_ret; + g_status = status; } static void hal_flash_set_addr(int page, int col) @@ -81,7 +88,7 @@ static void set32(volatile unsigned int *addr, unsigned int val) static uint32_t get32(volatile unsigned int *addr) { (void)addr; - return 0; /* MDR status: no error */ + return g_status; /* MDR: the NAND status byte */ } /* The real ext_flash_erase() from hal/nxp_p1021.c (extracted). */ @@ -96,7 +103,7 @@ START_TEST (test_erase_advances_through_blocks) { int ret; - mock_reset(0); + mock_reset(0, STATUS_OK); ret = ext_flash_erase(0, 2 * (int)TEST_BLOCK_SIZE); @@ -112,7 +119,23 @@ START_TEST (test_erase_stops_on_command_error) { int ret; - mock_reset(-1); + mock_reset(-1, STATUS_OK); + + ret = ext_flash_erase(0, 2 * (int)TEST_BLOCK_SIZE); + + ck_assert_int_eq(ret, -1); + ck_assert_int_eq(g_page_calls, 1); + ck_assert_int_eq(g_pages[0], 0); +} +END_TEST + +START_TEST (test_erase_stops_on_status_fail) +{ + int ret; + + /* The command sequence completes (cmd_ret 0) but the NAND reports an + * erase failure in the status byte (DQ0 clear). */ + mock_reset(0, STATUS_ERASE_FAIL); ret = ext_flash_erase(0, 2 * (int)TEST_BLOCK_SIZE); @@ -129,6 +152,7 @@ Suite *p1021_erase_suite(void) tcase_add_test(tc, test_erase_advances_through_blocks); tcase_add_test(tc, test_erase_stops_on_command_error); + tcase_add_test(tc, test_erase_stops_on_status_fail); tcase_set_timeout(tc, 10); suite_add_tcase(s, tc); return s; diff --git a/tools/unit-tests/unit-p1021-fcm-bytes.c b/tools/unit-tests/unit-p1021-fcm-bytes.c index 9ffdf3c3..ea3197e6 100644 --- a/tools/unit-tests/unit-p1021-fcm-bytes.c +++ b/tools/unit-tests/unit-p1021-fcm-bytes.c @@ -220,6 +220,8 @@ static void setup(void) flash_idx = 0; g_fbcr_n = 0; set32(ELBC_LTESR, ELBC_LTESR_CC); /* FCM commands complete instantly */ + /* MDR: DQ0+DQ1 set = program/erase success, not write-protected */ + set32(ELBC_MDR, 0x03); } static void teardown(void) diff --git a/tools/unit-tests/unit-update-ram-noramboot.c b/tools/unit-tests/unit-update-ram-noramboot.c index ce3d0de9..284c3a1d 100644 --- a/tools/unit-tests/unit-update-ram-noramboot.c +++ b/tools/unit-tests/unit-update-ram-noramboot.c @@ -81,6 +81,8 @@ static void reset_mock_stats(void) { wolfBoot_panicked = 0; wolfBoot_staged_ok = 0; + mock_max_read_addr = 0; + mock_max_read_len = 0; } static void prepare_flash(void) @@ -212,9 +214,13 @@ START_TEST (test_noramboot_fallback_to_lower_version) { wolfBoot_start(); /* A failed high-version boot image must not block fallback to the - * valid lower-version update image (F-12922). */ + * valid lower-version update image (F-12922). The image loaded to RAM + * must come from the UPDATE partition payload, not the oversize BOOT + * partition. */ ck_assert(wolfBoot_staged_ok); ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_uint_eq(mock_max_read_addr, + (uintptr_t)WOLFBOOT_PARTITION_UPDATE_ADDRESS + IMAGE_HEADER_SIZE); cleanup_flash(); } END_TEST From d316af656717e402afaa18dccc5326f2f65838ef Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 16 Sep 2026 14:40:50 +0200 Subject: [PATCH 40/49] PR 892: TFES PxCI check + unique test temp files - src/x86/ata.c: verify PxCI is clear in the TFES path before scrubbing the DMA buffer (symmetric with the success path; prevents scrubbing while the HBA may still reference the buffer) - unit-update-ram-nofixed-noramboot.c: append PID to /tmp filenames to avoid collisions under parallel make -j or shared runners --- src/x86/ata.c | 10 ++++++++-- tools/unit-tests/unit-update-ram-nofixed-noramboot.c | 11 +++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/x86/ata.c b/src/x86/ata.c index 6a00eb27..34fc1992 100644 --- a/src/x86/ata.c +++ b/src/x86/ata.c @@ -298,13 +298,19 @@ int ata_cmd_complete_async() if (!ata_async_info.in_progress) return ATA_ERR_OP_NOT_IN_PROGRESS; ata = &ATA_Drv[ata_async_info.drv]; + slot = ata_async_info.slot; if (mmio_read32(AHCI_PxIS(ata->ahci_base, ata->ahci_port)) & AHCI_PORT_IS_TFES) { + /* Task-file error: verify the HBA has retired the command (PxCI + * clear) before scrubbing the DMA buffer it may still reference. */ + if ((mmio_read32(AHCI_PxCI(ata->ahci_base, ata->ahci_port)) & + (1 << slot)) != 0) + return ATA_ERR_BUSY; ret = -1; goto done; } - slot = ata_async_info.slot; - if ((mmio_read32(AHCI_PxCI(ata->ahci_base, ata->ahci_port)) & (1 << slot)) != 0) + if ((mmio_read32(AHCI_PxCI(ata->ahci_base, ata->ahci_port)) & + (1 << slot)) != 0) return ATA_ERR_BUSY; ret = 0; diff --git a/tools/unit-tests/unit-update-ram-nofixed-noramboot.c b/tools/unit-tests/unit-update-ram-nofixed-noramboot.c index b3f02006..3a9ed6aa 100644 --- a/tools/unit-tests/unit-update-ram-nofixed-noramboot.c +++ b/tools/unit-tests/unit-update-ram-nofixed-noramboot.c @@ -107,12 +107,19 @@ static void reset_mock_stats(void) static void prepare_flash(void) { int ret; + char ext_path[64]; + char int_path[64]; - ret = mmap_file("/tmp/wolfboot-unit-ext-file-nofixed-noramboot.bin", + snprintf(ext_path, sizeof(ext_path), + "/tmp/wolfboot-unit-ext-file-nofixed-noramboot-%d.bin", (int)getpid()); + snprintf(int_path, sizeof(int_path), + "/tmp/wolfboot-unit-int-file-nofixed-noramboot-%d.bin", (int)getpid()); + + ret = mmap_file(ext_path, (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", + ret = mmap_file(int_path, (void *)(uintptr_t)MOCK_ADDRESS_BOOT, WOLFBOOT_PARTITION_SIZE + IMAGE_HEADER_SIZE, NULL); ck_assert_int_ge(ret, 0); From 6814b503efd24ba11a709e0490e8706a8c6964dc Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 16 Sep 2026 16:18:49 +0200 Subject: [PATCH 41/49] PR 892: address 6 Fenrir review findings - hal/nxp_p1021.c: fix ONFI status polarity (bit 0 set = fail, bit 7 clear = protected; was checking bits 0+1 set = success) - src/libwolfboot.c: move partition state APIs under HAVE_PARTITION_TRAILERS so CUSTOM_PARTITION_TRAILER without WOLFBOOT_FIXED_PARTITIONS links - src/x86/ata.c: do not zeroize DMA buffer when another async op is in progress (buffer still owned by first command's transfer) - unit-p1021-fcm-bytes/erase-advance: update mocks to ONFI status bytes - unit-update-ram-custom-trailer: add BOOT selection assertion via mock_max_read_addr, PID-suffixed temp files, unlink in cleanup - unit-update-ram-nofixed-noramboot: unlink PID-specific temp files --- hal/nxp_p1021.c | 20 +++++++------- src/libwolfboot.c | 4 +-- src/x86/ata.c | 7 +++-- tools/unit-tests/Makefile | 2 +- tools/unit-tests/unit-p1021-erase-advance.c | 9 ++++--- tools/unit-tests/unit-p1021-fcm-bytes.c | 4 +-- .../unit-update-ram-custom-trailer.c | 27 ++++++++++++++++--- .../unit-update-ram-nofixed-noramboot.c | 9 +++++++ 8 files changed, 57 insertions(+), 25 deletions(-) diff --git a/hal/nxp_p1021.c b/hal/nxp_p1021.c index e5e52cda..09b05b66 100644 --- a/hal/nxp_p1021.c +++ b/hal/nxp_p1021.c @@ -336,11 +336,11 @@ enum elbc_amask_sizes { #define NAND_CMD_READSTART 0x30 /* Extended command for large page devices */ -/* NAND device status byte (JEDEC), returned in MDR by the RS/RSW ops: - * DQ0 clear = program/erase failed, DQ1 clear = write protected. A good - * status has both bits set. */ -#define NAND_STATUS_FAIL (1 << 0) /* DQ0: 0 = program/erase fail */ -#define NAND_STATUS_WPS (1 << 1) /* DQ1: 0 = write protected */ +/* NAND device status byte (ONFI): bit 0 set = program/erase failed, + * bit 7 clear = write protected. Success requires bit 0 clear and + * bit 7 set. */ +#define NAND_STATUS_FAIL (1 << 0) /* DQ0: 1 = program/erase fail */ +#define NAND_STATUS_WP_N (1 << 7) /* DQ7: 0 = write protected */ /* DDR */ @@ -1711,11 +1711,10 @@ int ext_flash_write(uintptr_t address, const uint8_t *data, int len) wolfBoot_printf("write page %d, col %d, status %x\n", page, col, status); #endif - /* DQ0 clear = program failed, DQ1 clear = write protected: the + /* DQ0 set = program failed, DQ7 clear = write protected: the * page did not program. Stop; retrying the same page fails the * same way. */ - if ((status & (NAND_STATUS_FAIL | NAND_STATUS_WPS)) != - (NAND_STATUS_FAIL | NAND_STATUS_WPS)) { + if ((status & NAND_STATUS_FAIL) || !(status & NAND_STATUS_WP_N)) { ret = -1; break; } @@ -1879,10 +1878,9 @@ int ext_flash_erase(uintptr_t address, int len) #ifdef DEBUG_EXT_FLASH wolfBoot_printf("erase page %d, status %x\n", page, status); #endif - /* DQ0 clear = erase failed, DQ1 clear = write protected: the block + /* DQ0 set = erase failed, DQ7 clear = write protected: the block * did not erase. Stop; erasing the same block fails the same way. */ - if ((status & (NAND_STATUS_FAIL | NAND_STATUS_WPS)) != - (NAND_STATUS_FAIL | NAND_STATUS_WPS)) { + if ((status & NAND_STATUS_FAIL) || !(status & NAND_STATUS_WP_N)) { ret = -1; break; } diff --git a/src/libwolfboot.c b/src/libwolfboot.c index 05162cbf..23a8263c 100644 --- a/src/libwolfboot.c +++ b/src/libwolfboot.c @@ -657,7 +657,7 @@ static void RAMFUNCTION set_partition_magic(uint8_t part) -#ifdef WOLFBOOT_FIXED_PARTITIONS +#ifdef HAVE_PARTITION_TRAILERS #ifdef __CCRX__ #pragma section FRAM #endif @@ -959,7 +959,7 @@ void RAMFUNCTION wolfBoot_success(void) #ifdef __CCRX__ #pragma section #endif -#endif /* WOLFBOOT_FIXED_PARTITIONS */ +#endif /* HAVE_PARTITION_TRAILERS */ #ifdef WOLFBOOT_PERSIST_FAILURE_STATUS /* Persistent failure diagnostics. diff --git a/src/x86/ata.c b/src/x86/ata.c index 34fc1992..119bdc5e 100644 --- a/src/x86/ata.c +++ b/src/x86/ata.c @@ -532,9 +532,12 @@ static int security_command_passphrase(int drv, uint8_t ata_cmd, } else if (ret == ATA_ERR_BUSY) { /* Command is in flight: scrub once the HBA retires it. */ ata_async_info.scrub_buffer = 1; + } else if (ata_async_info.in_progress) { + /* Another async op is in progress: the buffer is still owned + * by its in-flight DMA transfer. Do not zeroize. */ } else { - /* Command never started (another async op in progress): the - * buffer is not referenced by any in-flight transfer. */ + /* Command failed to start and no async op is in progress: + * the buffer is not referenced by any in-flight transfer. */ ata_security_buffer_zeroize(); } return ret; diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index 41ed6946..56195c23 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -1349,7 +1349,7 @@ p1021_erase_extract.h: ../../hal/nxp_p1021.c sed -n '/#define NAND_CMD_BLOCK_ERASE1 /p' $< >> $@ sed -n '/#define NAND_CMD_BLOCK_ERASE2 /p' $< >> $@ sed -n '/#define FLASH_PAGE_SIZE /p' $< >> $@ - sed -n '/#define NAND_STATUS_WPS /p' $< >> $@ + sed -n '/#define NAND_STATUS_WP_N /p' $< >> $@ sed -n '/#define NAND_STATUS_FAIL /p' $< >> $@ p1021_erase_fn_extract.h: ../../hal/nxp_p1021.c diff --git a/tools/unit-tests/unit-p1021-erase-advance.c b/tools/unit-tests/unit-p1021-erase-advance.c index 49c8772a..f82845d3 100644 --- a/tools/unit-tests/unit-p1021-erase-advance.c +++ b/tools/unit-tests/unit-p1021-erase-advance.c @@ -47,10 +47,11 @@ static int g_cmd_calls; static int g_cmd_ret; static uint32_t g_status; -/* JEDEC status byte: DQ0 set = program/erase ok, DQ1 set = not protected. - * 0x03 is a clean success; 0x02 is an erase/program failure (DQ0 clear). */ -#define STATUS_OK 0x03 -#define STATUS_ERASE_FAIL 0x02 +/* ONFI status byte: DQ0 set = program/erase fail, DQ7 clear = protected. + * 0x80 is a clean success (no fail, not protected); 0x81 is an + * erase/program failure (DQ0 set). */ +#define STATUS_OK 0x80 +#define STATUS_ERASE_FAIL 0x81 static void mock_reset(int cmd_ret, uint32_t status) { diff --git a/tools/unit-tests/unit-p1021-fcm-bytes.c b/tools/unit-tests/unit-p1021-fcm-bytes.c index ea3197e6..7bfc9ff1 100644 --- a/tools/unit-tests/unit-p1021-fcm-bytes.c +++ b/tools/unit-tests/unit-p1021-fcm-bytes.c @@ -220,8 +220,8 @@ static void setup(void) flash_idx = 0; g_fbcr_n = 0; set32(ELBC_LTESR, ELBC_LTESR_CC); /* FCM commands complete instantly */ - /* MDR: DQ0+DQ1 set = program/erase success, not write-protected */ - set32(ELBC_MDR, 0x03); + /* MDR: DQ0 clear = no fail, DQ7 set = not write protected */ + set32(ELBC_MDR, 0x80); } static void teardown(void) diff --git a/tools/unit-tests/unit-update-ram-custom-trailer.c b/tools/unit-tests/unit-update-ram-custom-trailer.c index 0b31d66d..634026a5 100644 --- a/tools/unit-tests/unit-update-ram-custom-trailer.c +++ b/tools/unit-tests/unit-update-ram-custom-trailer.c @@ -130,6 +130,8 @@ static void reset_mock_stats(void) mock_flash_protect_called = 0; mock_flash_protect_addr = 0; mock_flash_protect_len = 0; + mock_max_read_addr = 0; + mock_max_read_len = 0; memset(mock_trailer_boot, 0, sizeof(mock_trailer_boot)); memset(mock_trailer_update, 0, sizeof(mock_trailer_update)); } @@ -137,12 +139,19 @@ static void reset_mock_stats(void) static void prepare_flash(void) { int ret; + char ext_path[64]; + char int_path[64]; - ret = mmap_file("/tmp/wolfboot-unit-ext-file-custom-trailer.bin", + snprintf(ext_path, sizeof(ext_path), + "/tmp/wolfboot-unit-ext-file-custom-trailer-%d.bin", (int)getpid()); + snprintf(int_path, sizeof(int_path), + "/tmp/wolfboot-unit-int-file-custom-trailer-%d.bin", (int)getpid()); + + ret = mmap_file(ext_path, (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-custom-trailer.bin", + ret = mmap_file(int_path, (void *)(uintptr_t)MOCK_ADDRESS_BOOT, WOLFBOOT_PARTITION_SIZE + IMAGE_HEADER_SIZE, NULL); ck_assert_int_ge(ret, 0); @@ -157,10 +166,19 @@ static void prepare_flash(void) static void cleanup_flash(void) { + char ext_path[64]; + char int_path[64]; + munmap((void *)WOLFBOOT_PARTITION_BOOT_ADDRESS, WOLFBOOT_PARTITION_SIZE + IMAGE_HEADER_SIZE); munmap((void *)WOLFBOOT_PARTITION_UPDATE_ADDRESS, WOLFBOOT_PARTITION_SIZE + IMAGE_HEADER_SIZE); + snprintf(ext_path, sizeof(ext_path), + "/tmp/wolfboot-unit-ext-file-custom-trailer-%d.bin", (int)getpid()); + snprintf(int_path, sizeof(int_path), + "/tmp/wolfboot-unit-int-file-custom-trailer-%d.bin", (int)getpid()); + unlink(ext_path); + unlink(int_path); } static int add_payload(uint8_t part, uint32_t version, uint32_t size) @@ -271,9 +289,12 @@ START_TEST(test_custom_trailer_invalid_update_falls_back_to_boot) wolfBoot_start(); - /* Falls back to boot partition (version 1) */ + /* Falls back to boot partition (version 1): the image loaded to RAM + * must come from the BOOT partition payload, not the corrupted UPDATE. */ ck_assert_int_eq(wolfBoot_staged_ok, 1); ck_assert_int_eq(wolfBoot_panicked, 0); + ck_assert_uint_eq(mock_max_read_addr, + (uintptr_t)WOLFBOOT_PARTITION_BOOT_ADDRESS); cleanup_flash(); } diff --git a/tools/unit-tests/unit-update-ram-nofixed-noramboot.c b/tools/unit-tests/unit-update-ram-nofixed-noramboot.c index 3a9ed6aa..816b6fc6 100644 --- a/tools/unit-tests/unit-update-ram-nofixed-noramboot.c +++ b/tools/unit-tests/unit-update-ram-nofixed-noramboot.c @@ -134,10 +134,19 @@ static void prepare_flash(void) static void cleanup_flash(void) { + char ext_path[64]; + char int_path[64]; + munmap((void *)WOLFBOOT_PARTITION_BOOT_ADDRESS, WOLFBOOT_PARTITION_SIZE + IMAGE_HEADER_SIZE); munmap((void *)WOLFBOOT_PARTITION_UPDATE_ADDRESS, WOLFBOOT_PARTITION_SIZE + IMAGE_HEADER_SIZE); + snprintf(ext_path, sizeof(ext_path), + "/tmp/wolfboot-unit-ext-file-nofixed-noramboot-%d.bin", (int)getpid()); + snprintf(int_path, sizeof(int_path), + "/tmp/wolfboot-unit-int-file-nofixed-noramboot-%d.bin", (int)getpid()); + unlink(ext_path); + unlink(int_path); } static int add_payload(uint8_t part, uint32_t version, uint32_t size) From c3f9717fb5fb598139ee33814b29e0480e0681a2 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 16 Sep 2026 17:19:16 +0200 Subject: [PATCH 42/49] PR 892: address 3 Fenrir review findings - libwolfboot.c: keep the partition state APIs (set/get_partition_state) available to CUSTOM_PARTITION_TRAILER / WOLFBOOT_NO_PARTITIONS builds; guard the fixed-partition APIs (sector flags, erase, trigger, success) behind WOLFBOOT_FIXED_PARTITIONS. Add the unit-custom-trailer-nopart compile target to lock in that configuration. - unit-p1021-fcm-bytes.c: add program tests for status 0x81 (fail) and 0x00 (write-protected), asserting no later page is programmed. - unit-p1021-erase-advance.c: add an erase test for status 0x00 (write-protected), asserting failure before advancing to the next block. --- include/image.h | 2 + include/wolfboot/wolfboot.h | 2 + src/libwolfboot.c | 10 ++ tools/unit-tests/Makefile | 15 +- tools/unit-tests/unit-custom-trailer-nopart.c | 141 ++++++++++++++++++ tools/unit-tests/unit-p1021-erase-advance.c | 21 ++- tools/unit-tests/unit-p1021-fcm-bytes.c | 48 ++++++ 7 files changed, 237 insertions(+), 2 deletions(-) create mode 100644 tools/unit-tests/unit-custom-trailer-nopart.c diff --git a/include/image.h b/include/image.h index ab5410f3..914d0d6c 100644 --- a/include/image.h +++ b/include/image.h @@ -1732,8 +1732,10 @@ int wolfBoot_open_self_address(struct wolfBoot_image *img, uint8_t *hdr, int wolfBoot_verify_integrity(struct wolfBoot_image *img); int wolfBoot_verify_authenticity(struct wolfBoot_image *img); int wolfBoot_set_partition_state(uint8_t part, uint8_t newst); +#ifdef WOLFBOOT_FIXED_PARTITIONS int wolfBoot_get_update_sector_flag(uint16_t sector, uint8_t *flag); int wolfBoot_set_update_sector_flag(uint16_t sector, uint8_t newflag); +#endif /* WOLFBOOT_FIXED_PARTITIONS */ #ifdef WOLFBOOT_ELF_FLASH_SCATTER /* Support for ELF scatter/gather format */ diff --git a/include/wolfboot/wolfboot.h b/include/wolfboot/wolfboot.h index d86812d6..004d248a 100644 --- a/include/wolfboot/wolfboot.h +++ b/include/wolfboot/wolfboot.h @@ -665,8 +665,10 @@ extern "C" { #include "image.h" #endif +#ifdef WOLFBOOT_FIXED_PARTITIONS void wolfBoot_update_trigger(void); void wolfBoot_success(void); +#endif /* WOLFBOOT_FIXED_PARTITIONS */ uint32_t wolfBoot_image_size(uint8_t *image); uint32_t wolfBoot_get_blob_version(uint8_t *blob); uint16_t wolfBoot_get_blob_type(uint8_t *blob); diff --git a/src/libwolfboot.c b/src/libwolfboot.c index 23a8263c..e9a36f3d 100644 --- a/src/libwolfboot.c +++ b/src/libwolfboot.c @@ -685,6 +685,11 @@ static void RAMFUNCTION set_partition_state(uint8_t part, uint8_t val) set_trailer_at(part, 1, val); } +/* Update-sector flag helpers and the fixed-partition APIs below need the + * fixed partition addresses and wolfboot_magic_trail, which a + * CUSTOM_PARTITION_TRAILER / WOLFBOOT_NO_PARTITIONS build does not define. + * The partition state APIs above stay available to custom-trailer builds. */ +#ifdef WOLFBOOT_FIXED_PARTITIONS /** * @brief Set the flags of an update sector. * @@ -710,6 +715,7 @@ static uint8_t* RAMFUNCTION get_update_sector_flags(uint32_t pos) { return (uint8_t *)get_trailer_at(PART_UPDATE, 2 + pos); } +#endif /* WOLFBOOT_FIXED_PARTITIONS */ /** * @brief Set the state of a partition. @@ -735,6 +741,7 @@ int RAMFUNCTION wolfBoot_set_partition_state(uint8_t part, uint8_t newst) return 0; } +#ifdef WOLFBOOT_FIXED_PARTITIONS /** * @brief Set the flag for sector * @@ -765,6 +772,7 @@ int RAMFUNCTION wolfBoot_set_update_sector_flag(uint16_t sector, set_update_sector_flags(pos, fl_value); return 0; } +#endif /* WOLFBOOT_FIXED_PARTITIONS */ /** * @brief Get the state of a partition. @@ -789,6 +797,7 @@ int RAMFUNCTION wolfBoot_get_partition_state(uint8_t part, uint8_t *st) return 0; } +#ifdef WOLFBOOT_FIXED_PARTITIONS /** * @brief Get the flag for sector * @@ -956,6 +965,7 @@ void RAMFUNCTION wolfBoot_success(void) wolfBoot_erase_encrypt_key(); #endif } +#endif /* WOLFBOOT_FIXED_PARTITIONS */ #ifdef __CCRX__ #pragma section #endif diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index 56195c23..93068196 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -66,7 +66,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-nofixed-noramboot unit-update-ram-noramboot unit-update-ram-custom-trailer 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-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-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 \ @@ -361,6 +361,16 @@ unit-update-ram-custom-trailer:CFLAGS+=-DMOCK_PARTITIONS -DWOLFBOOT_NO_SIGN \ -DWOLFBOOT_RAMBOOT_MAX_SIZE=WOLFBOOT_PARTITION_SIZE \ -DWOLFBOOT_ORIGIN=MOCK_ADDRESS_BOOT \ -DBOOTLOADER_PARTITION_SIZE=WOLFBOOT_PARTITION_SIZE +# CUSTOM_PARTITION_TRAILER + WOLFBOOT_NO_PARTITIONS: no WOLFBOOT_FIXED_PARTITIONS, +# so wolfboot_magic_trail and the fixed partition addresses are excluded from +# libwolfboot.c. Proves the partition state API compiles and works through the +# custom get/set_trailer_at backend without fixed partitions (F-1130279967). +unit-custom-trailer-nopart:CFLAGS+=-DMOCK_PARTITIONS -DWOLFBOOT_NO_SIGN \ + -DUNIT_TEST_AUTH -DWOLFBOOT_HASH_SHA256 -DPRINTF_ENABLED \ + -DWOLFBOOT_NO_PARTITIONS \ + -DCUSTOM_PARTITION_TRAILER \ + -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 \ @@ -946,6 +956,9 @@ unit-update-ram-noramboot: ../../include/target.h unit-update-ram-noramboot.c unit-update-ram-custom-trailer: ../../include/target.h unit-update-ram-custom-trailer.c gcc -o $@ unit-update-ram-custom-trailer.c ../../src/image.c $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha256.c $(CFLAGS) $(LDFLAGS) +unit-custom-trailer-nopart: ../../include/target.h unit-custom-trailer-nopart.c + gcc -o $@ unit-custom-trailer-nopart.c ../../src/image.c $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha256.c $(CFLAGS) $(LDFLAGS) + unit-update-flash-hwswap: ../../include/target.h unit-update-flash-hwswap.c gcc -o $@ unit-update-flash-hwswap.c ../../src/image.c $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha256.c $(CFLAGS) $(LDFLAGS) diff --git a/tools/unit-tests/unit-custom-trailer-nopart.c b/tools/unit-tests/unit-custom-trailer-nopart.c new file mode 100644 index 00000000..5a0d624a --- /dev/null +++ b/tools/unit-tests/unit-custom-trailer-nopart.c @@ -0,0 +1,141 @@ +/* unit-custom-trailer-nopart.c + * + * Compile + behaviour gate for the CUSTOM_PARTITION_TRAILER / + * WOLFBOOT_NO_PARTITIONS configuration: no WOLFBOOT_FIXED_PARTITIONS, so + * wolfboot_magic_trail and the fixed partition addresses are excluded from + * libwolfboot.c. The partition state API must still compile and work through + * the externally-defined get/set_trailer_at backend, while the + * fixed-partition functions (sector flags, erase, trigger, success) are + * absent. + */ +#ifndef WOLFBOOT_HASH_SHA256 + #define WOLFBOOT_HASH_SHA256 +#endif + +#define NO_FORK 1 + +#include +#include +#include +#include +#include +#include + +#include "target.h" + +#include "user_settings.h" +#include "wolfboot/wolfboot.h" + +/* Custom partition trailer backend (mocked). + * Layout per partition: [state(1)][magic(4)] + * get_trailer_at(part, 0) -> magic (base+1 .. base+4) + * get_trailer_at(part, 1) -> state (base+0) + */ +static uint8_t mock_trailer_boot[5]; +static uint8_t mock_trailer_update[5]; + +uint8_t* get_trailer_at(uint8_t part, uint32_t at) +{ + uint8_t *base = (part == PART_BOOT) ? mock_trailer_boot + : mock_trailer_update; + if (at == 0) + return &base[1]; + return &base[at - 1]; +} + +void set_trailer_at(uint8_t part, uint32_t at, uint8_t val) +{ + uint8_t *base = (part == PART_BOOT) ? mock_trailer_boot + : mock_trailer_update; + if (at == 0) + base[1] = val; + else + base[at - 1] = val; +} + +void set_partition_magic(uint8_t part) +{ + uint8_t *base = (part == PART_BOOT) ? mock_trailer_boot + : mock_trailer_update; + /* WOLFBOOT_MAGIC_TRAIL = 0x544F4F42 on LE: 'B','O','O','T' */ + base[1] = 'B'; + base[2] = 'O'; + base[3] = 'O'; + base[4] = 'T'; + (void)part; +} + +#include "libwolfboot.c" +#include "unit-mock-flash.c" + +static void reset_trailers(void) +{ + memset(mock_trailer_boot, 0, sizeof(mock_trailer_boot)); + memset(mock_trailer_update, 0, sizeof(mock_trailer_update)); +} + +/* State API round-trips through the custom backend with no fixed + * partitions present. */ +START_TEST(test_set_get_partition_state) +{ + uint8_t st = 0; + + reset_trailers(); + + /* No magic yet: set writes the magic then the state. */ + ck_assert_int_eq(wolfBoot_set_partition_state(PART_BOOT, + IMG_STATE_TESTING), 0); + ck_assert_int_eq(wolfBoot_get_partition_state(PART_BOOT, &st), 0); + ck_assert_uint_eq(st, IMG_STATE_TESTING); + + /* Update partition is independent. */ + ck_assert_int_eq(wolfBoot_set_partition_state(PART_UPDATE, + IMG_STATE_UPDATING), 0); + ck_assert_int_eq(wolfBoot_get_partition_state(PART_UPDATE, &st), 0); + ck_assert_uint_eq(st, IMG_STATE_UPDATING); + + /* PART_NONE is rejected. */ + ck_assert_int_eq(wolfBoot_set_partition_state(PART_NONE, 0), -1); + ck_assert_int_eq(wolfBoot_get_partition_state(PART_NONE, &st), -1); + + /* get on a partition without magic is rejected. */ + reset_trailers(); + ck_assert_int_eq(wolfBoot_get_partition_state(PART_BOOT, &st), -1); +} +END_TEST + +/* Fixed-partition APIs must be absent in this configuration. */ +START_TEST(test_fixed_partition_apis_absent) +{ +#ifndef WOLFBOOT_FIXED_PARTITIONS + ck_assert_int_eq(0, 0); +#else + ck_abort_msg("WOLFBOOT_FIXED_PARTITIONS must be undefined here"); +#endif +} +END_TEST + +int main(int argc, char *argv[]) +{ + int failed; + Suite *s; + TCase *tc; + SRunner *sr; + + s = suite_create("custom-trailer-nopart"); + tc = tcase_create("state-api"); + + tcase_add_checked_fixture(tc, reset_trailers, NULL); + tcase_add_test(tc, test_set_get_partition_state); + tcase_add_test(tc, test_fixed_partition_apis_absent); + suite_add_tcase(s, tc); + + sr = srunner_create(s); + srunner_set_fork_status(sr, CK_NOFORK); + srunner_run_all(sr, CK_NORMAL); + failed = srunner_ntests_failed(sr); + srunner_free(sr); + (void)argc; + (void)argv; + return (failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/tools/unit-tests/unit-p1021-erase-advance.c b/tools/unit-tests/unit-p1021-erase-advance.c index f82845d3..fdb4ed10 100644 --- a/tools/unit-tests/unit-p1021-erase-advance.c +++ b/tools/unit-tests/unit-p1021-erase-advance.c @@ -49,9 +49,10 @@ static uint32_t g_status; /* ONFI status byte: DQ0 set = program/erase fail, DQ7 clear = protected. * 0x80 is a clean success (no fail, not protected); 0x81 is an - * erase/program failure (DQ0 set). */ + * erase/program failure (DQ0 set); 0x00 is write-protected (DQ7 clear). */ #define STATUS_OK 0x80 #define STATUS_ERASE_FAIL 0x81 +#define STATUS_WP_PROTECTED 0x00 static void mock_reset(int cmd_ret, uint32_t status) { @@ -146,6 +147,23 @@ START_TEST (test_erase_stops_on_status_fail) } END_TEST +START_TEST (test_erase_stops_on_write_protected) +{ + int ret; + + /* The command sequence completes (cmd_ret 0) but the NAND reports the + * block as write-protected in the status byte (DQ7 clear). The erase + * must fail and not advance to the next block. */ + mock_reset(0, STATUS_WP_PROTECTED); + + ret = ext_flash_erase(0, 2 * (int)TEST_BLOCK_SIZE); + + ck_assert_int_eq(ret, -1); + ck_assert_int_eq(g_page_calls, 1); + ck_assert_int_eq(g_pages[0], 0); +} +END_TEST + Suite *p1021_erase_suite(void) { Suite *s = suite_create("p1021 erase advance"); @@ -154,6 +172,7 @@ Suite *p1021_erase_suite(void) tcase_add_test(tc, test_erase_advances_through_blocks); tcase_add_test(tc, test_erase_stops_on_command_error); tcase_add_test(tc, test_erase_stops_on_status_fail); + tcase_add_test(tc, test_erase_stops_on_write_protected); tcase_set_timeout(tc, 10); suite_add_tcase(s, tc); return s; diff --git a/tools/unit-tests/unit-p1021-fcm-bytes.c b/tools/unit-tests/unit-p1021-fcm-bytes.c index 7bfc9ff1..1d96ef34 100644 --- a/tools/unit-tests/unit-p1021-fcm-bytes.c +++ b/tools/unit-tests/unit-p1021-fcm-bytes.c @@ -459,6 +459,52 @@ START_TEST(test_p1021_write_unaligned) } END_TEST +/* A program that reports failure in the status byte (DQ0 set) must stop: + * the first page is the last programmed, the loop does not advance to the + * next page. setup() pins MDR to 0x80 (success), so override it here. */ +START_TEST(test_p1021_write_status_fail) +{ + uint8_t data[2 * 1024]; + size_t i; + + fill(data, sizeof(data), 0x50); + /* DQ0 set = program failed, DQ7 set = not protected. */ + set32(ELBC_MDR, 0x81); + + ck_assert_int_eq(ext_flash_write(0, data, 600), -1); + + /* Only the first page was programmed; the loop stopped before page 1. */ + ck_assert_int_eq(g_fbcr_n, 1); + ck_assert_uint_eq(g_fbcr_log[0], 0); + for (i = 0; i < 512; i++) + ck_assert_uint_eq(g_nand[i], data[i]); + for (i = 0; i < 512; i++) + ck_assert_uint_eq(NAND(1, i), 0xFF); +} +END_TEST + +/* A program on a write-protected block (DQ7 clear) must stop the same way: + * no later page is programmed. */ +START_TEST(test_p1021_write_write_protected) +{ + uint8_t data[2 * 1024]; + size_t i; + + fill(data, sizeof(data), 0x60); + /* DQ0 clear = no fail, DQ7 clear = write protected. */ + set32(ELBC_MDR, 0x00); + + ck_assert_int_eq(ext_flash_write(0, data, 600), -1); + + ck_assert_int_eq(g_fbcr_n, 1); + ck_assert_uint_eq(g_fbcr_log[0], 0); + for (i = 0; i < 512; i++) + ck_assert_uint_eq(g_nand[i], data[i]); + for (i = 0; i < 512; i++) + ck_assert_uint_eq(NAND(1, i), 0xFF); +} +END_TEST + /* A full-page read from column 0 must keep BC = 0 (full page + spare, * the only ECC-checking setting). */ START_TEST(test_p1021_read_full_page) @@ -573,6 +619,8 @@ Suite *p1021_fcm_suite(void) tcase_add_test(tc, test_p1021_write_partial); tcase_add_test(tc, test_p1021_write_multipart); tcase_add_test(tc, test_p1021_write_unaligned); + tcase_add_test(tc, test_p1021_write_status_fail); + tcase_add_test(tc, test_p1021_write_write_protected); tcase_add_test(tc, test_p1021_read_full_page); tcase_add_test(tc, test_p1021_read_short_spare_loaded); tcase_add_test(tc, test_p1021_read_multipart); From 7feb9cbf67a66ef6c6f02e80ab192029355022cb Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 16 Sep 2026 18:17:48 +0200 Subject: [PATCH 43/49] Mark wolfBoot_invalidate_hdr_cache RAMFUNCTION Called from the RAM-resident update path (update_ram.c) under EXT_FLASH; place the body in RAM so it runs after flash remap instead of faulting. Verification: built unit-custom-trailer-nopart (EXT_FLASH) - compiles clean; imx95_m7 (EXT_FLASH, CI repro) - compiles clean. --- include/image.h | 2 +- src/image.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/image.h b/include/image.h index 914d0d6c..ba55399d 100644 --- a/include/image.h +++ b/include/image.h @@ -1752,7 +1752,7 @@ uint16_t wolfBoot_get_header(struct wolfBoot_image *img, uint16_t type, uint8_t #ifdef EXT_FLASH /* Drop the cached external image header so the next open reloads it. */ -void wolfBoot_invalidate_hdr_cache(void); +void RAMFUNCTION wolfBoot_invalidate_hdr_cache(void); #endif /* Find the key slot ID based on the SHA hash of the key. */ diff --git a/src/image.c b/src/image.c index a028f2dc..dc48fc86 100644 --- a/src/image.c +++ b/src/image.c @@ -1097,7 +1097,7 @@ static uint8_t *fetch_hdr_cpy(struct wolfBoot_image *img) * 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) +void RAMFUNCTION wolfBoot_invalidate_hdr_cache(void) { hdr_cpy_done = 0; } From 5f231a890c091a2f505639ec1c6b7451ca430613 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 16 Sep 2026 18:31:59 +0200 Subject: [PATCH 44/49] Fix: keep wolfBoot API declarations visible to app builds c3f9717f guarded the success/trigger/sector-flag declarations behind WOLFBOOT_FIXED_PARTITIONS, but wolfboot.h includes target.h only under __WOLFBOOT, so test-apps (compiled without __WOLFBOOT) never saw the macro and failed with implicit-declaration errors on imx95_m7, mcxa, mcxw and mcxw-tz. Unguard the header declarations; keep the definitions guarded and move wolfBoot_success out of the fixed-partition guard since it only uses the partition-state API. --- include/image.h | 2 -- include/wolfboot/wolfboot.h | 2 -- src/libwolfboot.c | 3 ++- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/include/image.h b/include/image.h index ba55399d..3389f999 100644 --- a/include/image.h +++ b/include/image.h @@ -1732,10 +1732,8 @@ int wolfBoot_open_self_address(struct wolfBoot_image *img, uint8_t *hdr, int wolfBoot_verify_integrity(struct wolfBoot_image *img); int wolfBoot_verify_authenticity(struct wolfBoot_image *img); int wolfBoot_set_partition_state(uint8_t part, uint8_t newst); -#ifdef WOLFBOOT_FIXED_PARTITIONS int wolfBoot_get_update_sector_flag(uint16_t sector, uint8_t *flag); int wolfBoot_set_update_sector_flag(uint16_t sector, uint8_t newflag); -#endif /* WOLFBOOT_FIXED_PARTITIONS */ #ifdef WOLFBOOT_ELF_FLASH_SCATTER /* Support for ELF scatter/gather format */ diff --git a/include/wolfboot/wolfboot.h b/include/wolfboot/wolfboot.h index 004d248a..d86812d6 100644 --- a/include/wolfboot/wolfboot.h +++ b/include/wolfboot/wolfboot.h @@ -665,10 +665,8 @@ extern "C" { #include "image.h" #endif -#ifdef WOLFBOOT_FIXED_PARTITIONS void wolfBoot_update_trigger(void); void wolfBoot_success(void); -#endif /* WOLFBOOT_FIXED_PARTITIONS */ uint32_t wolfBoot_image_size(uint8_t *image); uint32_t wolfBoot_get_blob_version(uint8_t *blob); uint16_t wolfBoot_get_blob_type(uint8_t *blob); diff --git a/src/libwolfboot.c b/src/libwolfboot.c index e9a36f3d..498171d7 100644 --- a/src/libwolfboot.c +++ b/src/libwolfboot.c @@ -941,6 +941,8 @@ void RAMFUNCTION wolfBoot_update_trigger(void) } } +#endif /* WOLFBOOT_FIXED_PARTITIONS */ + /** * @brief Success function. * @@ -965,7 +967,6 @@ void RAMFUNCTION wolfBoot_success(void) wolfBoot_erase_encrypt_key(); #endif } -#endif /* WOLFBOOT_FIXED_PARTITIONS */ #ifdef __CCRX__ #pragma section #endif From 928b9d959012ff72486ba60f23063bd3afdc2690 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 16 Sep 2026 19:53:59 +0200 Subject: [PATCH 45/49] Guard security_command_passphrase against in-flight async DMA Check ata_async_info.in_progress before preparing the slot or touching the shared buffer, so a second security command cannot clobber the passphrase still being DMA'd. With that guard an async call always means in flight (exec_cmd_slot_ex sets in_progress and returns BUSY), so the post-command scrub is unconditional for the async path. --- src/x86/ata.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/x86/ata.c b/src/x86/ata.c index 119bdc5e..535cca92 100644 --- a/src/x86/ata.c +++ b/src/x86/ata.c @@ -497,7 +497,14 @@ static int security_command_passphrase(int drv, uint8_t ata_cmd, struct ata_drive *ata = &ATA_Drv[drv]; size_t passphrase_len = 0; int ret; - int slot = prepare_cmd_h2d_slot(drv, buffer, + int slot; + + /* A second security command must not touch the shared buffer while + * an async transfer is in flight: prepare_cmd_h2d_slot() and the + * memcpy below would clobber the passphrase still being DMA'd. */ + if (ata_async_info.in_progress) + return ATA_ERR_OP_IN_PROGRESS; + slot = prepare_cmd_h2d_slot(drv, buffer, ATA_SECURITY_COMMAND_LEN, 1); memset(buffer, 0, ATA_SECURITY_COMMAND_LEN); if (master) @@ -529,16 +536,9 @@ static int security_command_passphrase(int drv, uint8_t ata_cmd, * the HBA and could corrupt the command still in progress. */ if (!async) { ata_security_buffer_zeroize(); - } else if (ret == ATA_ERR_BUSY) { + } else { /* Command is in flight: scrub once the HBA retires it. */ ata_async_info.scrub_buffer = 1; - } else if (ata_async_info.in_progress) { - /* Another async op is in progress: the buffer is still owned - * by its in-flight DMA transfer. Do not zeroize. */ - } else { - /* Command failed to start and no async op is in progress: - * the buffer is not referenced by any in-flight transfer. */ - ata_security_buffer_zeroize(); } return ret; } From bb2e1a70f8cbd01ddee66e44e61ea7817761e358 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 16 Sep 2026 19:53:59 +0200 Subject: [PATCH 46/49] Fix DQ0 wording in p1021 erase-fail test comment STATUS_ERASE_FAIL is 0x81, so the fail bit (DQ0) is set, not clear. --- tools/unit-tests/unit-p1021-erase-advance.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/unit-tests/unit-p1021-erase-advance.c b/tools/unit-tests/unit-p1021-erase-advance.c index fdb4ed10..ab0320d4 100644 --- a/tools/unit-tests/unit-p1021-erase-advance.c +++ b/tools/unit-tests/unit-p1021-erase-advance.c @@ -136,7 +136,7 @@ START_TEST (test_erase_stops_on_status_fail) int ret; /* The command sequence completes (cmd_ret 0) but the NAND reports an - * erase failure in the status byte (DQ0 clear). */ + * erase failure in the status byte (DQ0 set). */ mock_reset(0, STATUS_ERASE_FAIL); ret = ext_flash_erase(0, 2 * (int)TEST_BLOCK_SIZE); From 80f109ea15f18f86ca6c522449582c4a2ab75ad6 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 16 Sep 2026 23:20:22 +0200 Subject: [PATCH 47/49] Drop tautology test from unit-custom-trailer-nopart test_fixed_partition_apis_absent only asserted the build config (0==0 when WOLFBOOT_FIXED_PARTITIONS is off), which the successful compile of the same target already proves. Reviewer nit. Verification: make -C tools/unit-tests unit-custom-trailer-nopart exits 0; suite runs 1 check, 0 failures. --- tools/unit-tests/unit-custom-trailer-nopart.c | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/tools/unit-tests/unit-custom-trailer-nopart.c b/tools/unit-tests/unit-custom-trailer-nopart.c index 5a0d624a..e27116b8 100644 --- a/tools/unit-tests/unit-custom-trailer-nopart.c +++ b/tools/unit-tests/unit-custom-trailer-nopart.c @@ -104,17 +104,6 @@ START_TEST(test_set_get_partition_state) } END_TEST -/* Fixed-partition APIs must be absent in this configuration. */ -START_TEST(test_fixed_partition_apis_absent) -{ -#ifndef WOLFBOOT_FIXED_PARTITIONS - ck_assert_int_eq(0, 0); -#else - ck_abort_msg("WOLFBOOT_FIXED_PARTITIONS must be undefined here"); -#endif -} -END_TEST - int main(int argc, char *argv[]) { int failed; @@ -127,7 +116,6 @@ int main(int argc, char *argv[]) tcase_add_checked_fixture(tc, reset_trailers, NULL); tcase_add_test(tc, test_set_get_partition_state); - tcase_add_test(tc, test_fixed_partition_apis_absent); suite_add_tcase(s, tc); sr = srunner_create(s); From d0ec587c7afde44ac59f0918ecdaea47a764e038 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 17 Sep 2026 00:10:32 +0200 Subject: [PATCH 48/49] Use literal exit codes in unit-custom-trailer-nopart Replace EXIT_SUCCESS/EXIT_FAILURE with 0/1; the test does not include and does not need the named macros. Verification: make -C tools/unit-tests unit-custom-trailer-nopart exits 0; suite runs 1 check, 0 failures. --- tools/unit-tests/unit-custom-trailer-nopart.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/unit-tests/unit-custom-trailer-nopart.c b/tools/unit-tests/unit-custom-trailer-nopart.c index e27116b8..0099143e 100644 --- a/tools/unit-tests/unit-custom-trailer-nopart.c +++ b/tools/unit-tests/unit-custom-trailer-nopart.c @@ -125,5 +125,5 @@ int main(int argc, char *argv[]) srunner_free(sr); (void)argc; (void)argv; - return (failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE; + return (failed == 0) ? 0 : 1; } From edf41b1c35837f2abe1606dacafdb7aa6ad310ef Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 17 Sep 2026 09:29:03 +0200 Subject: [PATCH 49/49] Verify partition isolation in custom-trailer state test After writing both partitions, re-read BOOT and confirm it still holds TESTING, so a backend that routes both IDs to the same trailer is caught (previously each read followed its own write). Verification: make -C tools/unit-tests unit-custom-trailer-nopart exits 0; suite runs 1 check, 0 failures. --- tools/unit-tests/unit-custom-trailer-nopart.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tools/unit-tests/unit-custom-trailer-nopart.c b/tools/unit-tests/unit-custom-trailer-nopart.c index 0099143e..16e05911 100644 --- a/tools/unit-tests/unit-custom-trailer-nopart.c +++ b/tools/unit-tests/unit-custom-trailer-nopart.c @@ -94,6 +94,11 @@ START_TEST(test_set_get_partition_state) ck_assert_int_eq(wolfBoot_get_partition_state(PART_UPDATE, &st), 0); ck_assert_uint_eq(st, IMG_STATE_UPDATING); + /* Writing UPDATE must not clobber BOOT: re-read BOOT and confirm it + * still holds TESTING, not the UPDATE value. */ + ck_assert_int_eq(wolfBoot_get_partition_state(PART_BOOT, &st), 0); + ck_assert_uint_eq(st, IMG_STATE_TESTING); + /* PART_NONE is rejected. */ ck_assert_int_eq(wolfBoot_set_partition_state(PART_NONE, 0), -1); ck_assert_int_eq(wolfBoot_get_partition_state(PART_NONE, &st), -1);