From a1bb7667d3285f07c19b439c1c01384e2acc6999 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 01:57:12 +0200 Subject: [PATCH 01/50] F-14149: compute the ARMORED witness triangular number in 64 bits --- src/image.c | 4 +- tools/unit-tests/Makefile | 12 +++ tools/unit-tests/unit-ct-compare.c | 114 +++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 tools/unit-tests/unit-ct-compare.c diff --git a/src/image.c b/src/image.c index ee23840c33..ff59436cfa 100644 --- a/src/image.c +++ b/src/image.c @@ -87,7 +87,9 @@ int NOINLINEFUNCTION image_CT_compare( budget--; } - expected_witness = (len * (len + 1U)) / 2U; /* sum(1..len) */ + /* 64-bit product: the triangular number overflows 32 bits at + * len >= 65536, which would poison the self-check below. */ + expected_witness = (uint32_t)(((uint64_t)len * ((uint64_t)len + 1U)) / 2U); len_is_zero = 1U ^ ((len | (0U - len)) >> 31); /* Folded twice, branch-free. */ diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index 1d9d80b84d..3ba19f0326 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -116,6 +116,7 @@ TESTS+=unit-p1021-fcm-bytes TESTS+=unit-ls1028a-xspi-write TESTS+=unit-zynq-erase-loop TESTS+=unit-zynq-ext-write +TESTS+=unit-ct-compare TESTS+=unit-versal-qspi-dma TESTS+=unit-imx8qm-usdhc-shim TESTS+=unit-imx8qm-flexspi-write @@ -1173,6 +1174,17 @@ zynq_erase_extract.h: ../../hal/zynq.c unit-zynq-erase-loop: unit-zynq-erase-loop.c zynq_erase_extract.h gcc -o $@ unit-zynq-erase-loop.c $(CFLAGS) $(LDFLAGS) +# unit-ct-compare runs the WOLFBOOT_ARMORED image_CT_compare() self-check +# at the 32-bit witness wrap boundary (len >= 65536). image.c drags in +# the full image dependency tree, so the ARMORED function (the first +# definition in the file) is extracted verbatim. +ct_compare_extract.h: ../../src/image.c + sed -n '/^int NOINLINEFUNCTION image_CT_compare(/,/^}/p' $< \ + | sed -n '1,/^}/p' > $@ + +unit-ct-compare: unit-ct-compare.c ct_compare_extract.h + gcc -o $@ unit-ct-compare.c $(CFLAGS) $(LDFLAGS) + # unit-zynq-ext-write runs the real ext_flash_write() from hal/zynq.c # (page chunks crossed physical NOR page boundaries and the # wrap clobbered the page start). Same extraction approach as the diff --git a/tools/unit-tests/unit-ct-compare.c b/tools/unit-tests/unit-ct-compare.c new file mode 100644 index 0000000000..02c79b005f --- /dev/null +++ b/tools/unit-tests/unit-ct-compare.c @@ -0,0 +1,114 @@ +/* unit-ct-compare.c + * + * Regression test: the WOLFBOOT_ARMORED image_CT_compare() self-check + * computed expected_witness = (len * (len + 1U)) / 2U in 32-bit + * arithmetic. For len >= 65536 the product truncates and the folded + * witness no longer matches the running sum, so byte-identical buffers + * were reported as unequal (fail-closed false reject). The loop itself + * is constant-time; this was a pure correctness defect in the witness. + * + * src/image.c drags in the full image dependency tree, so the Makefile + * extracts the ARMORED function verbatim; the test exercises it at the + * wrap boundary and beyond. + * + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#include +#include +#include +#include + +#define NOINLINEFUNCTION __attribute__((noinline)) + +/* Accumulator seed, mirrored from the definition in src/image.c. */ +#define CT_SENTINEL 0xA5C3F000U + +#include "ct_compare_extract.h" + +#define CT_BUF_SIZE 131072U + +START_TEST(test_ct_compare_equal_lengths){ + uint8_t *a = malloc(CT_BUF_SIZE); + uint8_t *b = malloc(CT_BUF_SIZE); + static const uint32_t lens[] = + {32U, 48U, 65535U, 65536U, 65537U, 131072U}; + unsigned li; + + ck_assert_ptr_nonnull(a); + ck_assert_ptr_nonnull(b); + + for (li = 0; li < sizeof(lens) / sizeof(lens[0]); li++) { + memset(a, 0x5AU, CT_BUF_SIZE); + memset(b, 0x5AU, CT_BUF_SIZE); + ck_assert_int_eq(image_CT_compare(a, b, lens[li]), 0); + } + + free(a); + free(b); +} +END_TEST + +START_TEST(test_ct_compare_unequal) +{ + uint8_t a[64]; + uint8_t b[64]; + + memset(a, 0x00, sizeof(a)); + memset(b, 0x00, sizeof(b)); + b[63] = 0x01; /* last byte differs: the loop must run to the end */ + ck_assert_int_ne(image_CT_compare(a, b, sizeof(a)), 0); +} +END_TEST + +START_TEST(test_ct_compare_zero_len) +{ + uint8_t a[1]; + + a[0] = 0; + ck_assert_int_ne(image_CT_compare(a, a, 0), 0); +} +END_TEST + +Suite *ct_compare_suite(void) +{ + Suite *s = suite_create("ct_compare"); + TCase *tc = tcase_create("armored"); + + suite_add_tcase(s, tc); + tcase_add_test(tc, test_ct_compare_equal_lengths); + tcase_add_test(tc, test_ct_compare_unequal); + tcase_add_test(tc, test_ct_compare_zero_len); + + return s; +} + +int main(void) +{ + int failed; + Suite *s = ct_compare_suite(); + SRunner *sr = srunner_create(s); + + srunner_run_all(sr, CK_NORMAL); + failed = srunner_ntests_failed(sr); + srunner_free(sr); + + return (failed == 0) ? 0 : 1; +} From b6f842ccbbd8b4a52a7ec5d2cf0615da746c6c84 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 02:02:08 +0200 Subject: [PATCH 02/50] F-14138: fix the DTCV base comment (2^13, not 2*13) --- src/sdhci.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sdhci.c b/src/sdhci.c index c078882da3..42af1fe713 100644 --- a/src/sdhci.c +++ b/src/sdhci.c @@ -311,7 +311,7 @@ static int sdhci_set_timeout(uint32_t timeout_us) } /* calculate the data timeout counter value */ - dtcv = 8192; /* 2*13 */ + dtcv = 8192; /* 2^13 */ for (i=0; i<15; i++) { if (timeout_val < (dtcv / tcfclk)) { break; From 1a8378ee580b1d2a521c88d428af3445414ccb0b Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 02:03:46 +0200 Subject: [PATCH 03/50] F-14133: drop the dead entry_out_set guard in the ELF check --- src/image.c | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/image.c b/src/image.c index ff59436cfa..42d4505780 100644 --- a/src/image.c +++ b/src/image.c @@ -2218,7 +2218,6 @@ int wolfBoot_check_flash_image_elf(uint8_t part, unsigned long* entry_out) uint8_t* exp_digest; int32_t stored_sha_len; int i; - int32_t entry_out_set = 0; uint8_t elfHdrBuf[sizeof(elfHeaderMaxBuf)]; uint8_t ph_buf[sizeof(elf64_program_header)]; /* Buffer for current PH */ uint8_t ph_next_buf[sizeof(elf64_program_header)]; /* Buffer for next PH */ @@ -2255,10 +2254,7 @@ int wolfBoot_check_flash_image_elf(uint8_t part, unsigned long* entry_out) entry_count = eh->ph_entry_count; entry_off = eh->ph_offset; ph_size = sizeof(elf32_program_header); - if (!entry_out_set) { - *entry_out = eh->entry; - entry_out_set = 1; - } + *entry_out = eh->entry; wolfBoot_printf("ELF: [CHECK] 32-bit, entry=0x%08X, " "ph_offset=0x%08X, ph_count=%u\n", (unsigned int)eh->entry, (unsigned int)entry_off, entry_count); @@ -2268,10 +2264,7 @@ int wolfBoot_check_flash_image_elf(uint8_t part, unsigned long* entry_out) entry_count = eh->ph_entry_count; entry_off = eh->ph_offset; ph_size = sizeof(elf64_program_header); - if (!entry_out_set) { - *entry_out = eh->entry; - entry_out_set = 1; - } + *entry_out = eh->entry; wolfBoot_printf("ELF: [CHECK] 64-bit, entry=0x%08lx, " "ph_offset=0x%08lx, ph_count=%d\n", (unsigned long)eh->entry, (unsigned long)entry_off, entry_count); From 6296f248c60aa76eb63408241ae0423a2ac2b082 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 02:05:18 +0200 Subject: [PATCH 04/50] F-14132: stage1 loader requires the full partition read before the jump --- stage1/loader_stage1.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stage1/loader_stage1.c b/stage1/loader_stage1.c index dcfe7c98f9..e5b38f66c9 100644 --- a/stage1/loader_stage1.c +++ b/stage1/loader_stage1.c @@ -107,9 +107,9 @@ int main(void) (uint8_t*)WOLFBOOT_ORIGIN, /* flash offset */ BOOTLOADER_PARTITION_SIZE /* boot-loader partition (entire) */ ); - ret = 0; + ret = (int)BOOTLOADER_PARTITION_SIZE; #endif - if (ret >= 0) { + if (ret == (int)BOOTLOADER_PARTITION_SIZE) { wolfboot_start = (uint32_t*)WOLFBOOT_STAGE1_LOAD_ADDR; #ifdef PRINTF_ENABLED wolfBoot_printf("Jumping to full wolfBoot at %p\n", wolfboot_start); From 0c8a9ae45f6e797b9806fe077fa80e1147e8519b Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 02:17:29 +0200 Subject: [PATCH 05/50] F-14131: pass a sector count, not a byte length, to FLASH_EraseSector --- hal/mcxa.c | 17 +++++- tools/unit-tests/mcxa_fsl_stub/fsl_common.h | 2 +- tools/unit-tests/mcxa_fsl_stub/fsl_romapi.h | 3 +- tools/unit-tests/unit-flash-write-mcxa.c | 68 ++++++++++++++++++++- 4 files changed, 83 insertions(+), 7 deletions(-) diff --git a/hal/mcxa.c b/hal/mcxa.c index 1f819103a3..de7a59c7b4 100644 --- a/hal/mcxa.c +++ b/hal/mcxa.c @@ -120,9 +120,20 @@ void RAMFUNCTION hal_flash_lock(void) int RAMFUNCTION hal_flash_erase(uint32_t address, int len) { - while ((address % 4) != 0) - address --; - if (FLASH_EraseSector(&pflash, address, len, kFLASH_ApiEraseKey) != kStatus_Success) + flash_config_t config; + uint32_t sector_count; + + if (len <= 0) + return -1; + /* FLASH_EraseSector takes a sector count, not a byte length; round + * the byte range up to whole sectors. */ + if (FLASH_GetFlashConfig(&pflash, &config) != kStatus_Success) + return -1; + sector_count = (uint32_t)len / config.sectorSize; + if (((uint32_t)len % config.sectorSize) != 0U) + sector_count++; + if (FLASH_EraseSector(&pflash, address, sector_count, kFLASH_ApiEraseKey) + != kStatus_Success) return -1; return 0; } diff --git a/tools/unit-tests/mcxa_fsl_stub/fsl_common.h b/tools/unit-tests/mcxa_fsl_stub/fsl_common.h index bce2daa2ec..ec5f4a8f38 100644 --- a/tools/unit-tests/mcxa_fsl_stub/fsl_common.h +++ b/tools/unit-tests/mcxa_fsl_stub/fsl_common.h @@ -11,7 +11,7 @@ typedef int status_t; #define kStatus_Success 0 typedef struct { - int dummy; + uint32_t sectorSize; } flash_config_t; #endif /* FSL_COMMON_STUB_H */ diff --git a/tools/unit-tests/mcxa_fsl_stub/fsl_romapi.h b/tools/unit-tests/mcxa_fsl_stub/fsl_romapi.h index bd864a6abb..f569118058 100644 --- a/tools/unit-tests/mcxa_fsl_stub/fsl_romapi.h +++ b/tools/unit-tests/mcxa_fsl_stub/fsl_romapi.h @@ -7,7 +7,8 @@ status_t FLASH_ProgramPhrase(flash_config_t *config, uint32_t start, uint8_t *src, uint32_t len); status_t FLASH_EraseSector(flash_config_t *config, uint32_t start, - uint32_t len, uint32_t key); + uint32_t sector_count, uint32_t key); +status_t FLASH_GetFlashConfig(flash_config_t *state, flash_config_t *config); #define kFLASH_ApiEraseKey 0x6b65796b diff --git a/tools/unit-tests/unit-flash-write-mcxa.c b/tools/unit-tests/unit-flash-write-mcxa.c index b98c2606bd..a48c1da0c6 100644 --- a/tools/unit-tests/unit-flash-write-mcxa.c +++ b/tools/unit-tests/unit-flash-write-mcxa.c @@ -49,6 +49,11 @@ #include "fsl_romapi.h" #include "image.h" +/* Records of the (mock) ROM erase calls issued by hal_flash_erase(). */ +static int erase_calls; +static uint32_t last_erase_start; +static uint32_t last_erase_count; + status_t FLASH_ProgramPhrase(flash_config_t *config, uint32_t start, uint8_t *src, uint32_t len) { @@ -58,9 +63,20 @@ status_t FLASH_ProgramPhrase(flash_config_t *config, uint32_t start, } status_t FLASH_EraseSector(flash_config_t *config, uint32_t start, - uint32_t len, uint32_t key) + uint32_t sector_count, uint32_t key) +{ + (void)config; (void)key; + erase_calls++; + last_erase_start = start; + last_erase_count = sector_count; + return kStatus_Success; +} + +status_t FLASH_GetFlashConfig(flash_config_t *state, flash_config_t *config) { - (void)config; (void)start; (void)len; (void)key; + (void)state; + /* i.MX RT5xx internal flash sector size */ + config->sectorSize = 4096U; return kStatus_Success; } @@ -80,6 +96,9 @@ static void setup(void) MAP_PRIVATE | MAP_ANONYMOUS | MAP_32BIT, -1, 0); ck_assert_ptr_ne(mock_flash, MAP_FAILED); memset(mock_flash, 0xFF, MOCK_FLASH_SIZE); + erase_calls = 0; + last_erase_start = 0; + last_erase_count = 0; } static void teardown(void) @@ -161,6 +180,48 @@ START_TEST(test_aligned_write_bulk_then_tail) } END_TEST +/* hal_flash_erase() must hand FLASH_EraseSector a sector count, not a byte + * length (the ROM API erases one sector per count unit). A 4 KiB request is + * one sector; a partial extra byte rounds up to the next sector. */ +START_TEST(test_erase_byte_len_converted_to_sector_count){ + uint32_t base = (uint32_t)(uintptr_t)mock_flash; + + ck_assert_int_eq(hal_flash_erase(base, 4096), 0); + ck_assert_int_eq(erase_calls, 1); + ck_assert_uint_eq(last_erase_start, base); + ck_assert_uint_eq(last_erase_count, 1); + + erase_calls = 0; + ck_assert_int_eq(hal_flash_erase(base, 4097), 0); + ck_assert_int_eq(erase_calls, 1); + ck_assert_uint_eq(last_erase_count, 2); + + erase_calls = 0; + ck_assert_int_eq(hal_flash_erase(base, 8192), 0); + ck_assert_int_eq(erase_calls, 1); + ck_assert_uint_eq(last_erase_count, 2); +} +END_TEST + +/* A zero-length erase request must be rejected without touching the ROM + * API (before the fix the old alignment loop ran and the byte length was + * passed straight through as the sector count). */ +START_TEST(test_erase_zero_len){ + uint32_t base = (uint32_t)(uintptr_t)mock_flash; + + ck_assert_int_eq(hal_flash_erase(base, 0), -1); + ck_assert_int_eq(erase_calls, 0); +} +END_TEST + +START_TEST(test_erase_negative_len){ + uint32_t base = (uint32_t)(uintptr_t)mock_flash; + + ck_assert_int_eq(hal_flash_erase(base, -1), -1); + ck_assert_int_eq(erase_calls, 0); +} +END_TEST + Suite *flash_write_suite(void) { Suite *s = suite_create("flash-write-mcxa"); @@ -170,6 +231,9 @@ Suite *flash_write_suite(void) tcase_add_test(tc, test_unaligned_write_spanning_two_words); tcase_add_test(tc, test_unaligned_write_single_word); tcase_add_test(tc, test_aligned_write_bulk_then_tail); + tcase_add_test(tc, test_erase_byte_len_converted_to_sector_count); + tcase_add_test(tc, test_erase_zero_len); + tcase_add_test(tc, test_erase_negative_len); suite_add_tcase(s, tc); return s; From 5e065c67fe7beed71a678543efdb0dd26c9fa233 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 02:17:29 +0200 Subject: [PATCH 06/50] F-14130: guard zero-length erase in the kinetis HAL --- hal/kinetis.c | 9 +- tools/unit-tests/Makefile | 7 + tools/unit-tests/unit-flash-erase-kinetis.c | 147 ++++++++++++++++++++ 3 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 tools/unit-tests/unit-flash-erase-kinetis.c diff --git a/hal/kinetis.c b/hal/kinetis.c index 91d83d4757..d076dbd6a2 100644 --- a/hal/kinetis.c +++ b/hal/kinetis.c @@ -358,13 +358,18 @@ void RAMFUNCTION hal_flash_lock(void) int RAMFUNCTION hal_flash_erase(uint32_t address, int len) { int idx = 0; + do_flash_init(); - do { + /* Pre-test guard: a do/while would erase one full sector for a + * zero/negative length request. */ + if (len <= 0) + return -1; + while (len > 0) { if (FLASH_Erase(&pflash, address + WOLFBOOT_SECTOR_SIZE * idx, WOLFBOOT_SECTOR_SIZE, kFTFx_ApiEraseKey) != kStatus_FTFx_Success) return -1; len -= WOLFBOOT_SECTOR_SIZE; idx++; - } while (len > 0); + } FTFx_CACHE_ClearCachePrefetchSpeculation(&pcache, 1); return 0; } diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index 3ba19f0326..1a1979b1f8 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -96,6 +96,7 @@ TESTS+=unit-flash-erase-g0 TESTS+=unit-flash-erase-c0 TESTS+=unit-flash-erase-u3 TESTS+=unit-flash-erase-mcxw +TESTS+=unit-flash-erase-kinetis TESTS+=unit-flash-m2354 TESTS+=unit-otp-keystore TESTS+=unit-otp-keystore-gen-zeroize @@ -683,6 +684,12 @@ unit-flash-erase-u3: unit-flash-erase-u3.c ../../hal/stm32u3.c ../../hal/stm32u3 # (not vendored) NXP MCUXpresso SDK headers are not needed. The erase command # takes a uint32_t flash address as a pointer, which is only a narrowing cast on # the 64-bit host. +kinetis_erase_extract.h: ../../hal/kinetis.c + sed -n '/^int RAMFUNCTION hal_flash_erase(/,/^}/p' $< > $@ + +unit-flash-erase-kinetis: unit-flash-erase-kinetis.c kinetis_erase_extract.h + gcc -o $@ unit-flash-erase-kinetis.c $(CFLAGS) $(LDFLAGS) + unit-flash-erase-mcxw: unit-flash-erase-mcxw.c ../../hal/mcxw.c gcc -o $@ unit-flash-erase-mcxw.c -Wno-int-to-pointer-cast \ $(CFLAGS) $(LDFLAGS) diff --git a/tools/unit-tests/unit-flash-erase-kinetis.c b/tools/unit-tests/unit-flash-erase-kinetis.c new file mode 100644 index 0000000000..f5d69045c7 --- /dev/null +++ b/tools/unit-tests/unit-flash-erase-kinetis.c @@ -0,0 +1,147 @@ +/* unit-flash-erase-kinetis.c + * + * Regression test: hal/kinetis.c hal_flash_erase() was a do/while loop, + * so one full WOLFBOOT_SECTOR_SIZE FLASH_Erase() was issued before `len` + * was ever tested; a zero (or negative) length request destroyed one + * sector at `address`. The loop is now pre-tested with a `len <= 0` + * guard matching the other in-tree HALs (stm32c0, stm32wb, mcxw, ...). + * + * The erase body is extracted verbatim from hal/kinetis.c; the NXP + * FTFx SDK calls are mocked to record every erase issued. + * + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#include +#include +#include + +/* NXP FTFx stand-ins */ +typedef int status_t; +#define kStatus_FTFx_Success 0 +#define kFTFx_ApiEraseKey 0x6b65796b + +typedef struct ftf_flash_config { + int dummy; +} ftf_flash_config_t; + +static ftf_flash_config_t pflash; +static ftf_flash_config_t pcache; + +static int erase_calls; +static uint32_t last_erase_addr; +static uint32_t last_erase_len; + +static void do_flash_init(void) +{ +} + +static status_t FLASH_Erase(ftf_flash_config_t *config, uint32_t start, + uint32_t len, uint32_t key) +{ + (void)config; + (void)key; + erase_calls++; + last_erase_addr = start; + last_erase_len = len; + return kStatus_FTFx_Success; +} + +static void FTFx_CACHE_ClearCachePrefetchSpeculation(ftf_flash_config_t *config, + uint32_t enable) +{ + (void)config; + (void)enable; +} + +/* Extracted verbatim from hal/kinetis.c */ +#define RAMFUNCTION +#define WOLFBOOT_SECTOR_SIZE 4096U +#include "kinetis_erase_extract.h" + +static void setup(void) +{ + erase_calls = 0; + last_erase_addr = 0; + last_erase_len = 0; +} + +static void teardown(void) +{ +} + +START_TEST(test_zero_len_erases_nothing){ + ck_assert_int_eq(hal_flash_erase(0x08000000U, 0), -1); + ck_assert_int_eq(erase_calls, 0); +} +END_TEST + +START_TEST(test_negative_len_erases_nothing) +{ + ck_assert_int_eq(hal_flash_erase(0x08000000U, -1), -1); + ck_assert_int_eq(erase_calls, 0); +} +END_TEST + +START_TEST(test_sub_sector_len_erases_one_sector) +{ + ck_assert_int_eq(hal_flash_erase(0x08000000U, 1), 0); + ck_assert_int_eq(erase_calls, 1); + ck_assert_uint_eq(last_erase_addr, 0x08000000U); + ck_assert_uint_eq(last_erase_len, WOLFBOOT_SECTOR_SIZE); +} +END_TEST + +START_TEST(test_multi_sector_len_erases_each_sector) +{ + ck_assert_int_eq(hal_flash_erase(0x08000000U, + 2 * (int)WOLFBOOT_SECTOR_SIZE + 7), 0); + ck_assert_int_eq(erase_calls, 3); + ck_assert_uint_eq(last_erase_addr, 0x08000000U + 2 * WOLFBOOT_SECTOR_SIZE); +} +END_TEST + +static Suite *kinetis_erase_suite(void) +{ + Suite *s = suite_create("kinetis_erase"); + TCase *tc = tcase_create("hal_flash_erase"); + + suite_add_tcase(s, tc); + tcase_add_checked_fixture(tc, setup, teardown); + tcase_add_test(tc, test_zero_len_erases_nothing); + tcase_add_test(tc, test_negative_len_erases_nothing); + tcase_add_test(tc, test_sub_sector_len_erases_one_sector); + tcase_add_test(tc, test_multi_sector_len_erases_each_sector); + + return s; +} + +int main(void) +{ + int number_failed; + Suite *s = kinetis_erase_suite(); + SRunner *sr = srunner_create(s); + + srunner_run_all(sr, CK_NORMAL); + number_failed = srunner_ntests_failed(sr); + srunner_free(sr); + + return (number_failed == 0) ? 0 : 1; +} From d17ea7aefe0d8c6c48a7e824bfe3c632caad622f Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 02:33:33 +0200 Subject: [PATCH 07/50] F-14129: derive the C0 erase page from the flash-relative offset --- hal/stm32c0.c | 6 +++++- tools/unit-tests/unit-flash-erase-c0.c | 27 +++++++++++++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/hal/stm32c0.c b/hal/stm32c0.c index b9a701218b..9405d45a53 100644 --- a/hal/stm32c0.c +++ b/hal/stm32c0.c @@ -80,6 +80,8 @@ #define FLASH_SECR (*(volatile uint32_t *)(FLASH_BASE + 0x80)) /* RM0490 - 3.7.13 - FLASH_SECR */ #endif /* !WOLFBOOT_UNIT_TEST_FLASH_ERASE */ +#define FLASHMEM_ADDRESS_SPACE (0x08000000) + #define FLASH_PAGE_SIZE (0x800) /* 2KB */ #define FLASH_PAGE_SIZE_SHIFT 11 /* (1 << FLASH_PAGE_SIZE_SHIFT) == FLASH_PAGE_SIZE*/ @@ -207,10 +209,12 @@ int RAMFUNCTION hal_flash_erase(uint32_t address, int len) uint32_t p; if (len == 0) return -1; + address -= FLASHMEM_ADDRESS_SPACE; end_address = address + len; for (p = address; p < end_address; p += FLASH_PAGE_SIZE) { uint32_t reg = FLASH_CR & (~(FLASH_CR_PNB_MASK << FLASH_CR_PNB_SHIFT)); - FLASH_CR = reg | ((p >> FLASH_PAGE_SIZE_SHIFT) << FLASH_CR_PNB_SHIFT) | FLASH_CR_PER; + FLASH_CR = reg | (((p >> FLASH_PAGE_SIZE_SHIFT) & FLASH_CR_PNB_MASK) << + FLASH_CR_PNB_SHIFT) | FLASH_CR_PER; DMB(); FLASH_CR |= FLASH_CR_STRT; flash_wait_complete(); diff --git a/tools/unit-tests/unit-flash-erase-c0.c b/tools/unit-tests/unit-flash-erase-c0.c index 9920d43972..d74bbdff99 100644 --- a/tools/unit-tests/unit-flash-erase-c0.c +++ b/tools/unit-tests/unit-flash-erase-c0.c @@ -47,6 +47,7 @@ static uint32_t mock_FLASH_SR; #define FLASH_SR mock_FLASH_SR /* Constants from hal/stm32c0.c (must mirror exactly). */ +#define FLASHMEM_ADDRESS_SPACE (0x08000000) #define FLASH_CR_STRT (1 << 16) #define FLASH_CR_PER (1 << 1) #define FLASH_CR_PNB_SHIFT 3 @@ -70,7 +71,9 @@ static void flash_wait_complete(void) } mock_FLASH_CR &= ~FLASH_CR_STRT; } -static void flash_clear_errors(void) {} +static void flash_clear_errors(void) +{ +} #include "../../hal/stm32c0.c" @@ -88,8 +91,7 @@ static void reset_mocks(void) } /* Erasing exactly one page must issue exactly one erase command. */ -START_TEST(test_erase_single_page_aligned) -{ +START_TEST(test_erase_single_page_aligned){ reset_mocks(); hal_flash_erase(0x08000000UL, FLASH_PAGE_SIZE); @@ -125,6 +127,24 @@ START_TEST(test_erase_unaligned_len_covers_last_page) } END_TEST +/* Regression: the page number must be derived from the flash-relative + * offset and confined to the PNB field (bits 9:3). Deriving it from the + * absolute 0x08000000 address ORs bit 19 (a reserved FLASH_CR bit) into + * every page-erase command. */ +START_TEST(test_erase_cr_no_stray_bits) +{ + uint32_t allowed; + + reset_mocks(); + hal_flash_erase(0x08000000UL, FLASH_PAGE_SIZE); + + ck_assert_int_eq(erase_log_n, 1); + allowed = FLASH_CR_PER | + (FLASH_CR_PNB_MASK << FLASH_CR_PNB_SHIFT) | FLASH_CR_STRT; + ck_assert_uint_eq(erase_cr[0] & ~allowed, 0); +} +END_TEST + Suite *flash_erase_c0_suite(void) { Suite *s = suite_create("flash-erase-c0"); @@ -133,6 +153,7 @@ Suite *flash_erase_c0_suite(void) tcase_add_test(tc, test_erase_single_page_aligned); tcase_add_test(tc, test_erase_two_pages_aligned); tcase_add_test(tc, test_erase_unaligned_len_covers_last_page); + tcase_add_test(tc, test_erase_cr_no_stray_bits); suite_add_tcase(s, tc); return s; From f0e2c4915060090530ed8a8d3b5c2eae033a1028 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 02:44:03 +0200 Subject: [PATCH 08/50] F-13656: bound e_phentsize to the program-header struct size An undersized e_phentsize made the last program-header loop iteration read past the validated table (and past the image buffer when the table ends at the image end). Reject such images with -3. Verification: tools/unit-tests/unit-elf-phentsize fails pre-fix (undersized ELF accepted, ret=0), passes post-fix (2/2); full make -C tools/unit-tests run green. --- src/elf.c | 7 +- tools/unit-tests/Makefile | 7 ++ tools/unit-tests/unit-elf-phentsize.c | 129 ++++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 tools/unit-tests/unit-elf-phentsize.c diff --git a/src/elf.c b/src/elf.c index cbd547fc91..383495d7a1 100644 --- a/src/elf.c +++ b/src/elf.c @@ -120,9 +120,14 @@ int elf_load_image_mmu(uint8_t *image, uint32_t image_sz, uintptr_t *pentry, entry_size = GET_H16(ph_entry_size); entry_count = GET_H16(ph_entry_count); - /* Validate program header table is within image bounds */ + /* Validate program header table is within image bounds and that + * each entry is big enough to hold one program header: a smaller + * e_phentsize would make the last loop iteration read past the + * validated table. */ if (ph_offset >= image_sz || entry_size == 0 || + entry_size < (is_elf32 ? (uint16_t)sizeof(elf32_program_header) : + (uint16_t)sizeof(elf64_program_header)) || entry_count > (image_sz / entry_size) || ((uint32_t)entry_count * entry_size) > (image_sz - ph_offset)) { return -3; /* program header table out of bounds */ diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index 1a1979b1f8..8a87536f58 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -107,6 +107,7 @@ TESTS+=unit-fwtpm-nv-oob TESTS+=unit-elf-bss-guard TESTS+=unit-elf-entry-inplace TESTS+=unit-elf-mmu-fail +TESTS+=unit-elf-phentsize TESTS+=unit-image-elf-scatter TESTS+=unit-arm-tee-psa-ipc TESTS+=unit-dice-token-size @@ -1819,6 +1820,12 @@ unit-elf-mmu-fail: unit-elf-mmu-fail.c ../../src/elf.c gcc -o $@ unit-elf-mmu-fail.c -I../../include -DWOLFBOOT_ELF \ -DARCH_FLASH_OFFSET=0 -DWOLFBOOT_NO_PRINTF -g $(LDFLAGS) +# unit-elf-phentsize: an undersized e_phentsize must be rejected before +# the program-header loop reads past the validated table (F-13656). +unit-elf-phentsize: unit-elf-phentsize.c ../../src/elf.c + gcc -o $@ unit-elf-phentsize.c -I../../include -DWOLFBOOT_ELF \ + -DARCH_FLASH_OFFSET=0 -DWOLFBOOT_NO_PRINTF -g $(LDFLAGS) + unit-image-elf-scatter: ../../include/target.h unit-image-elf-scatter.c gcc -o $@ unit-image-elf-scatter.c $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha256.c \ $(CFLAGS) $(LDFLAGS) diff --git a/tools/unit-tests/unit-elf-phentsize.c b/tools/unit-tests/unit-elf-phentsize.c new file mode 100644 index 0000000000..b4cc227225 --- /dev/null +++ b/tools/unit-tests/unit-elf-phentsize.c @@ -0,0 +1,129 @@ +/* unit-elf-phentsize.c + * + * Regression test for F-13656: elf_load_image_mmu() never constrained + * e_phentsize against the program-header struct size, so an + * undersized entry stride made the last loop iteration read past the + * validated program-header table (and past the image buffer when the + * table ends at the image end). The loader must reject such images. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#include +#include +#include + +#include "elf.h" + +/* Pull in elf.c directly (avoids a separate link step). */ +#include "../../src/elf.c" + +/* 4 KiB is plenty for the ELF64 header + 1 program header + segment data. */ +#define TEST_IMG_SIZE 4096 + +static uint8_t g_image[TEST_IMG_SIZE]; +static uint8_t g_target[8]; + +/* Build a minimal ELF64 with one 8-byte PT_LOAD segment whose data sits + * at image offset 256. entry_size overrides e_phentsize. */ +static void build_elf(uint16_t entry_size) +{ + elf64_header *hdr; + elf64_program_header *ph; + + memset(g_image, 0, sizeof(g_image)); + memset(g_target, 0, sizeof(g_target)); + + hdr = (elf64_header *)g_image; + memcpy(hdr->ident, ELF_IDENT_STR, 4); + hdr->ident[ELF_CLASS_OFF] = ELF_CLASS_64; + hdr->ident[5] = ELF_ENDIAN_LITTLE; + hdr->type = ELF_HET_EXEC; + hdr->version = 1; + hdr->entry = 0x1000; + hdr->ph_offset = sizeof(elf64_header); /* 64 */ + hdr->ph_entry_size = entry_size; + hdr->ph_entry_count = 1; + + ph = (elf64_program_header *)(g_image + sizeof(elf64_header)); + ph->type = ELF_PT_LOAD; + ph->flags = 0; + ph->offset = 256; + ph->vaddr = (uint64_t)(uintptr_t)g_target; + ph->paddr = ph->vaddr; + ph->file_size = 8; + ph->mem_size = 8; + ph->align = 1; + + memset(g_image + 256, 0xA5, 8); +} + +/* An e_phentsize smaller than the program-header struct must be + * rejected: the loop would read past the validated table. */ +START_TEST(test_undersized_phentsize_rejected){ + uintptr_t entry = 0; + int ret; + + build_elf(4); + + ret = elf_load_image_mmu(g_image, sizeof(g_image), &entry, NULL); + + ck_assert_int_eq(ret, -3); + ck_assert_int_eq(entry, 0); +} +END_TEST + +/* A conformant e_phentsize must still load: the new bound must not + * over-reject. */ +START_TEST(test_valid_phentsize_accepted) +{ + uintptr_t entry = 0; + int ret; + + build_elf((uint16_t)sizeof(elf64_program_header)); + + ret = elf_load_image_mmu(g_image, sizeof(g_image), &entry, NULL); + + ck_assert_int_eq(ret, 0); + ck_assert_int_eq(entry, 0x1000); + ck_assert_mem_eq(g_target, g_image + 256, 8); +} +END_TEST + +Suite *elf_phentsize_suite(void) +{ + Suite *s = suite_create("ELF phentsize"); + TCase *tc = tcase_create("phentsize-bound"); + tcase_add_test(tc, test_undersized_phentsize_rejected); + tcase_add_test(tc, test_valid_phentsize_accepted); + tcase_set_timeout(tc, 10); + suite_add_tcase(s, tc); + return s; +} + +int main(void) +{ + int fails; + Suite *s = elf_phentsize_suite(); + SRunner *sr = srunner_create(s); + srunner_run_all(sr, CK_NORMAL); + fails = srunner_ntests_failed(sr); + srunner_free(sr); + return fails; +} From 6025f3540ffb43561ba5890ceee8c18cb4bd3650 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 02:47:16 +0200 Subject: [PATCH 09/50] F-13655: index the flash double-word fast path from the current offset With an unaligned start address the byte-wise prefix leaves i % 4 != 0, so dst[i >> 2] programmed the double word below the intended address and left the tail bytes of the unit unprogrammed. Derive src/dst from address + i, matching the already-fixed else branch. Verification: new unit-stm32g4-write test_write_unaligned_start fails pre-fix (memcmp mismatch), 5/5 post-fix; full make -C tools/unit-tests run green. --- hal/stm32g0.c | 8 ++++---- hal/stm32g4.c | 8 ++++---- tools/unit-tests/unit-stm32g4-write.c | 20 ++++++++++++++++++++ 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/hal/stm32g0.c b/hal/stm32g0.c index aea0fc8bc4..03d83380ed 100644 --- a/hal/stm32g0.c +++ b/hal/stm32g0.c @@ -142,11 +142,11 @@ int RAMFUNCTION hal_flash_write(uint32_t address, const uint8_t *data, int len) flash_clear_errors(); if ((len - i >= 8) && ((((address + i) & 0x07) == 0) && ((((uint32_t)data) + i) & 0x07) == 0)) { - src = (uint32_t *)data; - dst = (uint32_t *)address; + src = (uint32_t *)(data + i); + dst = (uint32_t *)(address + i); flash_wait_complete(); - dst[i >> 2] = src[i >> 2]; - dst[(i >> 2) + 1] = src[(i >> 2) + 1]; + dst[0] = src[0]; + dst[1] = src[1]; flash_wait_complete(); i+=8; } else { diff --git a/hal/stm32g4.c b/hal/stm32g4.c index a880c0ad5e..3372607fb4 100644 --- a/hal/stm32g4.c +++ b/hal/stm32g4.c @@ -56,11 +56,11 @@ int RAMFUNCTION hal_flash_write(uint32_t address, const uint8_t *data, int len) flash_clear_errors(); if ((len - i >= 8) && ((((address + i) & 0x07) == 0) && ((((uint32_t)data) + i) & 0x07) == 0)) { - src = (uint32_t *)data; - dst = (uint32_t *)address; + src = (uint32_t *)(data + i); + dst = (uint32_t *)(address + i); flash_wait_complete(); - dst[i >> 2] = src[i >> 2]; - dst[(i >> 2) + 1] = src[(i >> 2) + 1]; + dst[0] = src[0]; + dst[1] = src[1]; flash_wait_complete(); i += 8; } else { diff --git a/tools/unit-tests/unit-stm32g4-write.c b/tools/unit-tests/unit-stm32g4-write.c index c6eb00ae4e..2d51e6dff5 100644 --- a/tools/unit-tests/unit-stm32g4-write.c +++ b/tools/unit-tests/unit-stm32g4-write.c @@ -179,6 +179,25 @@ START_TEST(test_write_64_full_units) } END_TEST +/* A write starting at an address with mod-8 residue 1: the byte-wise + * prefix leaves i = 7, where the fast path fires with i % 4 != 0. + * Pre-fix the double word was programmed 3 bytes below the intended + * address and the last 3 bytes of the unit were never programmed. */ +START_TEST(test_write_unaligned_start) +{ + int i; + + ck_assert_int_eq(hal_flash_write((uint32_t)(uintptr_t)(g_flash_mem + 1), + g_data + 1, 16), 0); + + ck_assert_int_eq(memcmp(g_flash_mem + 1, g_data + 1, 16), 0); + ck_assert_uint_eq(g_flash_mem[0], 0x12); + for (i = 17; i < FLASH_MEM_SZ; i++) + ck_assert_uint_eq(g_flash_mem[i], 0x12); + ck_assert_int_eq(canary_in_flash(), 0); +} +END_TEST + Suite *stm32g4_write_suite(void) { Suite *s = suite_create("stm32g4-write"); @@ -189,6 +208,7 @@ Suite *stm32g4_write_suite(void) tcase_add_test(tc, test_write_58_partial_word_padded); tcase_add_test(tc, test_write_3_single_word_padded); tcase_add_test(tc, test_write_64_full_units); + tcase_add_test(tc, test_write_unaligned_start); suite_add_tcase(s, tc); return s; From 1d1977c241cab85afaeb7e56c486aa2a230f2cd6 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 02:49:58 +0200 Subject: [PATCH 10/50] F-13654: index the NVM word fast path from the current offset With matched-but-unaligned address/data residues the byte-wise prefix leaves i % 4 != 0, so dst[i >> 2] stored the word below the intended address, dropped the tail bytes of the unit, and issued an unaligned NVM store (HardFault on M0+). Derive src/dst from address + i, matching the already-correct else branch. Verification: new matched-alignment test fails pre-fix in both unit-flash-write-samr21 and unit-flash-write-same51 (tail bytes stay 0xFF), 3/3 post-fix; full make -C tools/unit-tests run green. --- hal/same51.c | 6 ++--- hal/samr21.c | 6 ++--- tools/unit-tests/unit-flash-write-same51.c | 29 ++++++++++++++++++++++ tools/unit-tests/unit-flash-write-samr21.c | 29 ++++++++++++++++++++++ 4 files changed, 64 insertions(+), 6 deletions(-) diff --git a/hal/same51.c b/hal/same51.c index a1950df99c..c1ba2edaa6 100644 --- a/hal/same51.c +++ b/hal/same51.c @@ -352,9 +352,9 @@ int RAMFUNCTION hal_flash_write(uint32_t address, const uint8_t *data, int len) NVMCTRLB = (NVMCMD_PBC | NVMCMD_KEY); while (i < len) { if ((len - i > 3) && ((((address + i) & 0x03) == 0) && ((((uint32_t)data) + i) & 0x03) == 0)) { - dst = (uint32_t *)address; - src = (uint32_t *)data; - dst[i >> 2] = src[i >> 2]; + dst = (uint32_t *)(address + i); + src = (uint32_t *)(data + i); + *dst = *src; i+=4; } else { uint32_t val; diff --git a/hal/samr21.c b/hal/samr21.c index 0ad4945e2b..4c116fa128 100644 --- a/hal/samr21.c +++ b/hal/samr21.c @@ -174,9 +174,9 @@ int RAMFUNCTION hal_flash_write(uint32_t address, const uint8_t *data, int len) NVMCTRLA_REG = (NVMCMD_PBC | NVMCMD_KEY); while (i < len) { if ((len - i > 3) && ((((address + i) & 0x03) == 0) && ((((uint32_t)data) + i) & 0x03) == 0)) { - dst = (uint32_t *)address; - src = (uint32_t *)data; - dst[i >> 2] = src[i >> 2]; + dst = (uint32_t *)(address + i); + src = (uint32_t *)(data + i); + *dst = *src; i+=4; } else { uint32_t val; diff --git a/tools/unit-tests/unit-flash-write-same51.c b/tools/unit-tests/unit-flash-write-same51.c index 3c750163a4..4f7e77d236 100644 --- a/tools/unit-tests/unit-flash-write-same51.c +++ b/tools/unit-tests/unit-flash-write-same51.c @@ -118,6 +118,34 @@ START_TEST(test_unaligned_write_mismatched_alignment) } END_TEST +/* Destination misaligned by 1 (mod 4), source buffer also misaligned by + * 1 (mod 4): after the first partial word the fast 32-bit path re-syncs + * with i % 4 != 0. Pre-fix the word was stored i % 4 bytes below the + * intended address and the tail bytes of the unit were never + * programmed. */ +START_TEST(test_unaligned_write_matched_alignment) +{ + uint8_t rawbuf[64]; + uint8_t *data = rawbuf; + uint32_t base = (uint32_t)(uintptr_t)mock_flash; + int i; + + while (((uintptr_t)data % 4) != 1) + data++; + for (i = 0; i < 12; i++) + data[i] = (uint8_t)(0xC0 + i); + + ck_assert_int_eq(hal_flash_write(base + 5, data, 8), 0); + + for (i = 0; i < 5; i++) + ck_assert_uint_eq(mock_flash[i], 0xFF); + for (i = 0; i < 8; i++) + ck_assert_uint_eq(mock_flash[5 + i], data[i]); + for (i = 13; i < MOCK_FLASH_SIZE; i++) + ck_assert_uint_eq(mock_flash[i], 0xFF); +} +END_TEST + /* A write that fits entirely inside a single flash word must still work: * buggy and fixed forms agree here (the fill loop runs to completion on * the first iteration), guarding against a fix that breaks the common @@ -148,6 +176,7 @@ Suite *flash_write_suite(void) tcase_add_checked_fixture(tc, setup, teardown); tcase_add_test(tc, test_unaligned_write_mismatched_alignment); + tcase_add_test(tc, test_unaligned_write_matched_alignment); tcase_add_test(tc, test_unaligned_write_single_word); suite_add_tcase(s, tc); diff --git a/tools/unit-tests/unit-flash-write-samr21.c b/tools/unit-tests/unit-flash-write-samr21.c index e974b4fc51..d13cf31169 100644 --- a/tools/unit-tests/unit-flash-write-samr21.c +++ b/tools/unit-tests/unit-flash-write-samr21.c @@ -118,6 +118,34 @@ START_TEST(test_unaligned_write_mismatched_alignment) } END_TEST +/* Destination misaligned by 1 (mod 4), source buffer also misaligned by + * 1 (mod 4): after the first partial word the fast 32-bit path re-syncs + * with i % 4 != 0. Pre-fix the word was stored i % 4 bytes below the + * intended address and the tail bytes of the unit were never + * programmed. */ +START_TEST(test_unaligned_write_matched_alignment) +{ + uint8_t rawbuf[64]; + uint8_t *data = rawbuf; + uint32_t base = (uint32_t)(uintptr_t)mock_flash; + int i; + + while (((uintptr_t)data % 4) != 1) + data++; + for (i = 0; i < 12; i++) + data[i] = (uint8_t)(0xC0 + i); + + ck_assert_int_eq(hal_flash_write(base + 5, data, 8), 0); + + for (i = 0; i < 5; i++) + ck_assert_uint_eq(mock_flash[i], 0xFF); + for (i = 0; i < 8; i++) + ck_assert_uint_eq(mock_flash[5 + i], data[i]); + for (i = 13; i < MOCK_FLASH_SIZE; i++) + ck_assert_uint_eq(mock_flash[i], 0xFF); +} +END_TEST + /* A write that fits entirely inside a single flash word must still work: * buggy and fixed forms agree here (the fill loop runs to completion on * the first iteration), guarding against a fix that breaks the common @@ -148,6 +176,7 @@ Suite *flash_write_suite(void) tcase_add_checked_fixture(tc, setup, teardown); tcase_add_test(tc, test_unaligned_write_mismatched_alignment); + tcase_add_test(tc, test_unaligned_write_matched_alignment); tcase_add_test(tc, test_unaligned_write_single_word); suite_add_tcase(s, tc); From 65e3b40361cbce0a1436da7cf6fb7a225b72dde6 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 06:47:51 +0200 Subject: [PATCH 11/50] F-12884: flush the nRF54L UART conversion buffer when full Input longer than the 128-byte conversion buffer was silently truncated: the loop stopped at the buffer edge and one raw write dropped the rest. Flush on full (and before a newline that no longer fits) and keep converting. Verification: new unit-nrf54l-uart-write fails pre-fix 3/4 (output capped at 128 bytes), 4/4 post-fix; full make -C tools/unit-tests run green. --- hal/nrf54l.c | 17 +- tools/unit-tests/Makefile | 9 ++ tools/unit-tests/unit-nrf54l-uart-write.c | 187 ++++++++++++++++++++++ 3 files changed, 208 insertions(+), 5 deletions(-) create mode 100644 tools/unit-tests/unit-nrf54l-uart-write.c diff --git a/hal/nrf54l.c b/hal/nrf54l.c index 3c2e91b410..f24a26ae84 100644 --- a/hal/nrf54l.c +++ b/hal/nrf54l.c @@ -111,22 +111,29 @@ void uart_write_device(int device, const char* buf, unsigned int sz) { static char buffer[UART_WRITE_BUF_SIZE]; int bufsz = 0; + int i; - for (int i = 0; i < (int)sz && bufsz < UART_WRITE_BUF_SIZE; i++) { + for (i = 0; i < (int)sz; i++) { char ch = (char) buf[i]; if (ch == '\r') continue; if (ch == '\n') { - if (bufsz >= (UART_WRITE_BUF_SIZE - 1)) - break; - + if (bufsz >= (UART_WRITE_BUF_SIZE - 1)) { + uart_write_raw(device, buffer, bufsz); + bufsz = 0; + } buffer[bufsz++] = '\r'; } + if (bufsz >= UART_WRITE_BUF_SIZE) { + uart_write_raw(device, buffer, bufsz); + bufsz = 0; + } buffer[bufsz++] = ch; } - uart_write_raw(device, buffer, bufsz); + if (bufsz > 0) + uart_write_raw(device, buffer, bufsz); } void uart_write(const char* buf, unsigned int sz) diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index 8a87536f58..86b516819e 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -107,6 +107,7 @@ TESTS+=unit-fwtpm-nv-oob TESTS+=unit-elf-bss-guard TESTS+=unit-elf-entry-inplace TESTS+=unit-elf-mmu-fail +TESTS+=unit-nrf54l-uart-write TESTS+=unit-elf-phentsize TESTS+=unit-image-elf-scatter TESTS+=unit-arm-tee-psa-ipc @@ -1388,6 +1389,14 @@ stm32g4_write_extract.h: ../../hal/stm32g4.c sed -n '/^static void RAMFUNCTION flash_clear_errors/,/^}/p' $< >> $@ sed -n '/^int RAMFUNCTION hal_flash_write/,/^}/p' $< >> $@ +# unit-nrf54l-uart-write: the conversion buffer must flush when full so +# long lines are not truncated (F-12884); uart_write_raw is mocked. +nrf54l_uart_write_extract.h: ../../hal/nrf54l.c + sed -n '/^void uart_write_device/,/^}/p' $< > $@ + +unit-nrf54l-uart-write: unit-nrf54l-uart-write.c nrf54l_uart_write_extract.h + gcc -o $@ unit-nrf54l-uart-write.c $(CFLAGS) $(LDFLAGS) + unit-stm32g4-write: unit-stm32g4-write.c stm32g4_write_extract.h gcc -o $@ unit-stm32g4-write.c $(CFLAGS) $(LDFLAGS) diff --git a/tools/unit-tests/unit-nrf54l-uart-write.c b/tools/unit-tests/unit-nrf54l-uart-write.c new file mode 100644 index 0000000000..82edf5b0aa --- /dev/null +++ b/tools/unit-tests/unit-nrf54l-uart-write.c @@ -0,0 +1,187 @@ +/* unit-nrf54l-uart-write.c + * + * Regression test for F-12884: uart_write_device() in hal/nrf54l.c + * stopped its conversion loop when the 128-byte buffer filled and did + * a single raw write, silently truncating any longer input (earlier + * still when newline expansion consumed the spare slot). The writer + * must flush the buffer when full and keep converting the rest. + * + * The real function is extracted by the Makefile; uart_write_raw() is + * a recording mock. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#include +#include +#include + +#define UART_WRITE_BUF_SIZE 128 + +/* Recording mock for the DMA raw writer (prototype first: the extracted + * function calls it before the definition below). */ +#define OUT_CAP 8192 +static uint8_t g_out[OUT_CAP]; +static int g_out_len; +static int g_writes; + +static void uart_write_raw(int device, const char* buf, unsigned int sz); + +/* The real function from hal/nrf54l.c (extracted by the Makefile). */ +#include "nrf54l_uart_write_extract.h" + +static void uart_write_raw(int device, const char* buf, unsigned int sz) +{ + (void)device; + ck_assert_int_le(g_out_len + (int)sz, OUT_CAP); + memcpy(g_out + g_out_len, buf, sz); + g_out_len += (int)sz; + g_writes++; +} + +static void setup(void) +{ + memset(g_out, 0, sizeof(g_out)); + g_out_len = 0; + g_writes = 0; +} + +static void teardown(void) +{ +} + +/* Expected transform: drop \r, expand \n to \r\n. */ +static int build_expected(const char* in, int inlen, uint8_t* out) +{ + int n = 0; + int i; + + for (i = 0; i < inlen; i++) { + if (in[i] == '\r') + continue; + if (in[i] == '\n') + out[n++] = '\r'; + out[n++] = (uint8_t)in[i]; + } + return n; +} + +/* A plain 300-byte line: pre-fix everything past byte 128 was dropped. */ +START_TEST(test_write_300_no_truncation){ + char in[300]; + uint8_t exp[300]; + int explen; + int i; + + for (i = 0; i < 300; i++) + in[i] = (char)('a' + (i % 26)); + explen = build_expected(in, 300, exp); + + uart_write_device(0, in, 300); + + ck_assert_int_eq(g_out_len, explen); + ck_assert_int_eq(g_writes, 3); + ck_assert_int_eq(memcmp(g_out, exp, explen), 0); +} +END_TEST + +/* 200 newlines: every one expands to CRLF, 400 bytes out. Pre-fix the + * expansion stopped early at the buffer boundary. */ +START_TEST(test_write_newline_expansion) +{ + char in[200]; + uint8_t exp[400]; + int explen; + int i; + + memset(in, '\n', sizeof(in)); + explen = build_expected(in, 200, exp); + + uart_write_device(0, in, 200); + + ck_assert_int_eq(g_out_len, explen); + ck_assert_int_eq(memcmp(g_out, exp, explen), 0); +} +END_TEST + +/* Mixed content with \r\n pairs straddling the 128-byte boundary: the + * flush must never split a CRLF pair across writes inconsistently, and + * the reassembled stream must match the transform exactly. */ +START_TEST(test_write_mixed_boundary) +{ + char in[260]; + uint8_t exp[520]; + int explen; + int i; + + for (i = 0; i < 260; i++) + in[i] = (i % 13 == 0) ? '\r' : (i % 7 == 0) ? '\n' : (char)('A' + i); + explen = build_expected(in, 260, exp); + + uart_write_device(0, in, 260); + + ck_assert_int_eq(g_out_len, explen); + ck_assert_int_eq(memcmp(g_out, exp, explen), 0); +} +END_TEST + +/* Short input stays a single write: the common path is unchanged. */ +START_TEST(test_write_short_single) +{ + const char* in = "boot ok\n"; + uint8_t exp[32]; + int explen; + + explen = build_expected(in, (int)strlen(in), exp); + + uart_write_device(0, in, (unsigned int)strlen(in)); + + ck_assert_int_eq(g_writes, 1); + ck_assert_int_eq(g_out_len, explen); + ck_assert_int_eq(memcmp(g_out, exp, explen), 0); +} +END_TEST + +Suite *nrf54l_uart_write_suite(void) +{ + Suite *s = suite_create("nrf54l-uart-write"); + TCase *tc = tcase_create("nrf54l-uart-write"); + + tcase_add_checked_fixture(tc, setup, teardown); + tcase_add_test(tc, test_write_300_no_truncation); + tcase_add_test(tc, test_write_newline_expansion); + tcase_add_test(tc, test_write_mixed_boundary); + tcase_add_test(tc, test_write_short_single); + suite_add_tcase(s, tc); + + return s; +} + +int main(void) +{ + int fails; + Suite *s = nrf54l_uart_write_suite(); + SRunner *sr = srunner_create(s); + + srunner_run_all(sr, CK_NORMAL); + fails = srunner_ntests_failed(sr); + srunner_free(sr); + + return fails; +} From 519552fabbc033603a9613bb0a3b845c145d7bff Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 06:51:15 +0200 Subject: [PATCH 12/50] F-12872: reject a repeated squashelf -r option A second -r replaced the ranges array without freeing the first (leak) and silently discarded its ranges. Reject the repeat with a pointer to the comma-separated form. Verification: new tools/squashelf/test-repeat-range.py fails pre-fix (repeated -r accepted), passes post-fix; full make -C tools/squashelf test green (8/8). --- tools/squashelf/Makefile | 1 + tools/squashelf/squashelf.c | 7 ++ tools/squashelf/test-repeat-range.py | 108 +++++++++++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 tools/squashelf/test-repeat-range.py diff --git a/tools/squashelf/Makefile b/tools/squashelf/Makefile index f7842c6f85..d7f975db6d 100644 --- a/tools/squashelf/Makefile +++ b/tools/squashelf/Makefile @@ -28,6 +28,7 @@ debug: all test: $(TARGET) python3 test-range-overflow.py ./$(TARGET) python3 test-align-overflow.py ./$(TARGET) + python3 test-repeat-range.py ./$(TARGET) $(TARGET): $(TARGET).o @echo "Building squashelf tool" diff --git a/tools/squashelf/squashelf.c b/tools/squashelf/squashelf.c index f3b27124d5..032d9b994d 100644 --- a/tools/squashelf/squashelf.c +++ b/tools/squashelf/squashelf.c @@ -430,6 +430,13 @@ int main(int argCount, char** argValues) noSht = 1; break; case 'r': { + if (ranges != NULL) { + fprintf(stderr, + "Only one -r option is supported: use a single " + "comma-separated range list\n"); + free(ranges); + return EXIT_FAILURE; + } hasRange = 1; if (!parseRangeArgument(optarg, &ranges, &rangeCount, verbose)) { diff --git a/tools/squashelf/test-repeat-range.py b/tools/squashelf/test-repeat-range.py new file mode 100644 index 0000000000..72524e7225 --- /dev/null +++ b/tools/squashelf/test-repeat-range.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +# test-repeat-range.py +# +# Regression test for F-12872: repeating the -r option in squashelf +# replaced the ranges array without freeing the previous one (leak) and +# silently discarded the earlier ranges. A second -r must now be +# rejected; a single comma-separated list remains the way to pass +# multiple ranges. +# +# Copyright (C) 2026 wolfSSL Inc. +# +# This file is part of wolfBoot. +# +# wolfBoot is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# wolfBoot is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + +import os +import struct +import subprocess +import sys +import tempfile + +EHSIZE = 64 +PHSIZE = 56 + + +def make_elf64(path, paddr, memsz, filesz=0x10): + ph_off = EHSIZE + seg_off = ph_off + PHSIZE + ident = b"\x7fELF" + bytes([2, 1, 1, 0]) + b"\x00" * 8 # ELF64, little-endian + ehdr = ident + struct.pack( + " 1 else "./squashelf" + rc = 0 + with tempfile.TemporaryDirectory() as d: + elf = os.path.join(d, "in.elf") + make_elf64(elf, paddr=0x50, memsz=0x100) + + # 1) A second -r must be rejected: pre-fix it succeeded, leaking + # the first array and silently filtering on the second range + # only. The second range alone keeps the segment, so a + # pre-fix run exits 0 (accepted) while a post-fix run exits + # non-zero (rejected). + if run(squashelf, ["-r", "0x400-0x500", "-r", "0x0-0x200"], + elf, os.path.join(d, "dup.out")) == 0: + print("FAIL: repeated -r option was accepted") + rc = 1 + else: + print("PASS: repeated -r option rejected") + + # 2) Regression guard: a single comma-separated range list must + # still work (the filter keeps a segment only when both its + # start and end fall inside one of the ranges). + if run(squashelf, ["-r", "0x0-0x200,0x400-0x500"], + elf, os.path.join(d, "list.out")) != 0: + print("FAIL: comma-separated range list was rejected") + rc = 1 + else: + print("PASS: comma-separated range list accepted") + + sys.exit(rc) + + +if __name__ == "__main__": + main() From 58061721eea68494dfb9a2ef137f855a9b0e1554 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 07:00:28 +0200 Subject: [PATCH 13/50] F-12871: split cross-bank flash writes at the bank boundary hal_flash_write() picked the bank from the start address and programmed the whole request on that bank, so a request crossing into bank 2 wrote its tail to the wrong bank. The exported writer now splits at FLASH_BANK2_BASE_REL into two per-bank passes; a failure in the first pass stops the split. Unit test extracts the dispatcher and mocks the per-bank pass: cross-boundary split, single-bank pass-through, and first-pass failure all covered; fails pre-fix. --- hal/stm32h7.c | 26 +++ tools/unit-tests/Makefile | 9 ++ tools/unit-tests/unit-stm32h7-flash-write.c | 166 ++++++++++++++++++++ 3 files changed, 201 insertions(+) create mode 100644 tools/unit-tests/unit-stm32h7-flash-write.c diff --git a/hal/stm32h7.c b/hal/stm32h7.c index 4383e4daa7..0241b7fcd0 100644 --- a/hal/stm32h7.c +++ b/hal/stm32h7.c @@ -90,7 +90,33 @@ static void RAMFUNCTION flash_program_off(uint8_t bank) } } +static int RAMFUNCTION hal_flash_write_part(uint32_t address, + const uint8_t *data, int len); + +/* + * hal_flash_write() dispatches across the bank boundary: a request that + * starts in bank 1 and extends into bank 2 is split at + * FLASH_BANK2_BASE_REL, because the programming path below selects one + * bank (FLASH_CR1/CR2) for the whole request and would write the tail + * into the wrong bank (F-12871). + */ int RAMFUNCTION hal_flash_write(uint32_t address, const uint8_t *data, int len) +{ + uint32_t bank2 = FLASH_BANK2_BASE_REL; + + if ((address < bank2) && ((address + (uint32_t)len) > bank2)) { + uint32_t first = bank2 - address; + + if (hal_flash_write_part(address, data, (int)first) != 0) + return -1; + return hal_flash_write_part(bank2, data + first, + len - (int)first); + } + return hal_flash_write_part(address, data, len); +} + +static int RAMFUNCTION hal_flash_write_part(uint32_t address, const uint8_t *data, + int len) { int i = 0, ii =0; uint32_t *src, *dst; diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index 86b516819e..24e4bbff0f 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -108,6 +108,7 @@ TESTS+=unit-elf-bss-guard TESTS+=unit-elf-entry-inplace TESTS+=unit-elf-mmu-fail TESTS+=unit-nrf54l-uart-write +TESTS+=unit-stm32h7-flash-write TESTS+=unit-elf-phentsize TESTS+=unit-image-elf-scatter TESTS+=unit-arm-tee-psa-ipc @@ -1397,6 +1398,14 @@ nrf54l_uart_write_extract.h: ../../hal/nrf54l.c unit-nrf54l-uart-write: unit-nrf54l-uart-write.c nrf54l_uart_write_extract.h gcc -o $@ unit-nrf54l-uart-write.c $(CFLAGS) $(LDFLAGS) +# unit-stm32h7-flash-write: the bank-boundary dispatcher splits cross-bank +# requests (F-12871); hal_flash_write_part is mocked. +stm32h7_flash_write_extract.h: ../../hal/stm32h7.c + sed -n '/^int RAMFUNCTION hal_flash_write(/,/^}/p' $< > $@ + +unit-stm32h7-flash-write: unit-stm32h7-flash-write.c stm32h7_flash_write_extract.h + gcc -o $@ unit-stm32h7-flash-write.c $(CFLAGS) $(LDFLAGS) + unit-stm32g4-write: unit-stm32g4-write.c stm32g4_write_extract.h gcc -o $@ unit-stm32g4-write.c $(CFLAGS) $(LDFLAGS) diff --git a/tools/unit-tests/unit-stm32h7-flash-write.c b/tools/unit-tests/unit-stm32h7-flash-write.c new file mode 100644 index 0000000000..cc19a96831 --- /dev/null +++ b/tools/unit-tests/unit-stm32h7-flash-write.c @@ -0,0 +1,166 @@ +/* unit-stm32h7-flash-write.c + * + * Regression test for F-12871: hal_flash_write() in hal/stm32h7.c picked + * the bank from the start address and programmed the whole request on + * that bank, so a request crossing the bank boundary wrote its tail to + * the wrong bank. The exported writer must now split such requests at + * FLASH_BANK2_BASE_REL. + * + * The dispatcher is extracted by the Makefile; hal_flash_write_part() + * is a recording mock. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#include +#include +#include + +#define RAMFUNCTION +#define FLASH_BANK_1 0 +#define FLASH_BANK_2 1 +#define FLASH_BANK2_BASE_REL 0x100000u + +/* The real dispatcher from hal/stm32h7.c (extracted by the Makefile); + * it calls hal_flash_write_part(), mocked below (prototype first). */ +static int hal_flash_write_part(uint32_t address, const uint8_t *data, int len); + +#include "stm32h7_flash_write_extract.h" + +/* Recording mock for the per-bank programming pass. */ +#define MAX_CALLS 4 +static int g_calls; +static uint32_t g_addr[MAX_CALLS]; +static int g_len[MAX_CALLS]; +static int g_first_ret; + +static int hal_flash_write_part(uint32_t address, const uint8_t *data, int len) +{ + int ret; + + (void)data; + ck_assert_int_le(g_calls, MAX_CALLS - 1); + g_addr[g_calls] = address; + g_len[g_calls] = len; + if (g_calls == 0) + ret = g_first_ret; + else + ret = 0; + g_calls++; + return ret; +} + +static void setup(void) +{ + g_calls = 0; + g_first_ret = 0; + memset(g_addr, 0, sizeof(g_addr)); + memset(g_len, 0, sizeof(g_len)); +} + +static void teardown(void) +{ +} + +/* A request crossing the boundary must be split: first pass up to the + * boundary on bank 1, remainder starting at the boundary on bank 2. */ +START_TEST(test_write_crosses_boundary){ + uint8_t data[64]; + + memset(data, 0xAA, sizeof(data)); + + ck_assert_int_eq(hal_flash_write(FLASH_BANK2_BASE_REL - 16, data, 32), 0); + ck_assert_int_eq(g_calls, 2); + ck_assert_uint_eq(g_addr[0], FLASH_BANK2_BASE_REL - 16); + ck_assert_int_eq(g_len[0], 16); + ck_assert_uint_eq(g_addr[1], FLASH_BANK2_BASE_REL); + ck_assert_int_eq(g_len[1], 16); +} +END_TEST + +/* A request fully inside bank 1 stays a single pass. */ +START_TEST(test_write_bank1_only) +{ + uint8_t data[16]; + + memset(data, 0xAA, sizeof(data)); + + ck_assert_int_eq(hal_flash_write(0x400, data, 16), 0); + ck_assert_int_eq(g_calls, 1); + ck_assert_uint_eq(g_addr[0], 0x400); + ck_assert_int_eq(g_len[0], 16); +} +END_TEST + +/* A request fully inside bank 2 stays a single pass. */ +START_TEST(test_write_bank2_only) +{ + uint8_t data[16]; + + memset(data, 0xAA, sizeof(data)); + + ck_assert_int_eq(hal_flash_write(FLASH_BANK2_BASE_REL + 0x400, data, 16), + 0); + ck_assert_int_eq(g_calls, 1); + ck_assert_uint_eq(g_addr[0], FLASH_BANK2_BASE_REL + 0x400); + ck_assert_int_eq(g_len[0], 16); +} +END_TEST + +/* A failure in the first pass must stop the split and propagate. */ +START_TEST(test_write_first_part_fails) +{ + uint8_t data[64]; + + memset(data, 0xAA, sizeof(data)); + g_first_ret = -1; + + ck_assert_int_eq(hal_flash_write(FLASH_BANK2_BASE_REL - 16, data, 32), + -1); + ck_assert_int_eq(g_calls, 1); +} +END_TEST + +Suite *stm32h7_flash_write_suite(void) +{ + Suite *s = suite_create("stm32h7-flash-write"); + TCase *tc = tcase_create("stm32h7-flash-write"); + + tcase_add_checked_fixture(tc, setup, teardown); + tcase_add_test(tc, test_write_crosses_boundary); + tcase_add_test(tc, test_write_bank1_only); + tcase_add_test(tc, test_write_bank2_only); + tcase_add_test(tc, test_write_first_part_fails); + suite_add_tcase(s, tc); + + return s; +} + +int main(void) +{ + int fails; + Suite *s = stm32h7_flash_write_suite(); + SRunner *sr = srunner_create(s); + + srunner_run_all(sr, CK_NORMAL); + fails = srunner_ntests_failed(sr); + srunner_free(sr); + + return fails; +} From 66b2205e9b79eb10ecf4c09285e25fd19a236159 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 07:04:58 +0200 Subject: [PATCH 14/50] F-12106: report flash programming errors on STM32U3 hal_flash_write() cleared errors before programming and waited for completion but never read the post-program status register, so OPERR/PROGERR/WRPERR/PGAERR/SIZERR/PGSERR faults were swallowed and a failed program was reported as success. Check FLASH_NS_SR after the wait, clear the flags, and return -1 on fault; reject addresses not aligned to the 8-byte double-word programming unit. Unit test extracts the writer with stubbed registers: success, PROGERR, WRPERR and misaligned-address cases; fails pre-fix. --- hal/stm32u3.c | 14 ++ tools/unit-tests/Makefile | 11 ++ tools/unit-tests/unit-stm32u3-flash-write.c | 167 ++++++++++++++++++++ 3 files changed, 192 insertions(+) create mode 100644 tools/unit-tests/unit-stm32u3-flash-write.c diff --git a/hal/stm32u3.c b/hal/stm32u3.c index 7fb9e3fea9..57d871b315 100644 --- a/hal/stm32u3.c +++ b/hal/stm32u3.c @@ -71,6 +71,11 @@ int RAMFUNCTION hal_flash_write(uint32_t address, const uint8_t *data, int len) int i = 0; uint32_t *dst; + /* Programming unit is a double word: the address must be 8-byte + * aligned, the stores below assume it. */ + if ((address & 7) != 0) + return -1; + dst = (uint32_t *)address; while (i < len) { @@ -103,6 +108,15 @@ int RAMFUNCTION hal_flash_write(uint32_t address, const uint8_t *data, int len) /* RM step 10: clear PG */ FLASH_NS_CR &= ~FLASH_CR_PG; + + /* RM step 11: a programming fault must be reported to the + * caller, not swallowed (F-12106). */ + if ((FLASH_NS_SR & (FLASH_SR_OPERR | FLASH_SR_PROGERR | + FLASH_SR_WRPERR | FLASH_SR_PGAERR | + FLASH_SR_SIZERR | FLASH_SR_PGSERR)) != 0) { + flash_clear_errors(); + return -1; + } i += 8; } return 0; diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index 24e4bbff0f..469ee1b390 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -109,6 +109,7 @@ TESTS+=unit-elf-entry-inplace TESTS+=unit-elf-mmu-fail TESTS+=unit-nrf54l-uart-write TESTS+=unit-stm32h7-flash-write +TESTS+=unit-stm32u3-flash-write TESTS+=unit-elf-phentsize TESTS+=unit-image-elf-scatter TESTS+=unit-arm-tee-psa-ipc @@ -1406,6 +1407,16 @@ stm32h7_flash_write_extract.h: ../../hal/stm32h7.c unit-stm32h7-flash-write: unit-stm32h7-flash-write.c stm32h7_flash_write_extract.h gcc -o $@ unit-stm32h7-flash-write.c $(CFLAGS) $(LDFLAGS) +# unit-stm32u3-flash-write: post-program status must be checked and +# misaligned addresses rejected (F-12106); flash registers are stubbed. +stm32u3_flash_write_extract.h: ../../hal/stm32u3.c + sed -n '/^static RAMFUNCTION void flash_wait_complete/,/^}/p' $< > $@ + sed -n '/^static void RAMFUNCTION flash_clear_errors/,/^}/p' $< >> $@ + sed -n '/^int RAMFUNCTION hal_flash_write/,/^}/p' $< >> $@ + +unit-stm32u3-flash-write: unit-stm32u3-flash-write.c stm32u3_flash_write_extract.h + gcc -o $@ unit-stm32u3-flash-write.c $(CFLAGS) $(LDFLAGS) + unit-stm32g4-write: unit-stm32g4-write.c stm32g4_write_extract.h gcc -o $@ unit-stm32g4-write.c $(CFLAGS) $(LDFLAGS) diff --git a/tools/unit-tests/unit-stm32u3-flash-write.c b/tools/unit-tests/unit-stm32u3-flash-write.c new file mode 100644 index 0000000000..44a706ded7 --- /dev/null +++ b/tools/unit-tests/unit-stm32u3-flash-write.c @@ -0,0 +1,167 @@ +/* unit-stm32u3-flash-write.c + * + * Regression test for F-12106: hal_flash_write() in hal/stm32u3.c cleared + * flash errors before programming and waited for completion, but never + * checked the post-program status register, so programming faults + * (OPERR/PROGERR/WRPERR/PGAERR/SIZERR/PGSERR) were swallowed and the + * write reported success. The writer must now return -1 when the status + * register carries an error bit, and reject addresses not aligned to the + * 8-byte double-word programming unit. + * + * The real functions are extracted by the Makefile; the flash registers + * are stubbed with plain variables. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#include +#include +#include +#include +#include + +#define RAMFUNCTION + +/* Stubbed flash registers. */ +static volatile uint32_t g_sr; +static volatile uint32_t g_cr; +#define FLASH_NS_SR g_sr +#define FLASH_NS_CR g_cr +#define ISB() +#define FLASH_SR_EOP (1 << 0) +#define FLASH_SR_OPERR (1 << 1) +#define FLASH_SR_PROGERR (1 << 3) +#define FLASH_SR_WRPERR (1 << 4) +#define FLASH_SR_PGAERR (1 << 5) +#define FLASH_SR_SIZERR (1 << 6) +#define FLASH_SR_PGSERR (1 << 7) +#define FLASH_SR_OPTWERR (1 << 13) +#define FLASH_SR_BSY (1 << 16) +#define FLASH_SR_WDW (1 << 17) +#define FLASH_CR_PG (1 << 0) + +/* The real functions from hal/stm32u3.c (extracted by the Makefile). */ +#include "stm32u3_flash_write_extract.h" + +static void setup(void) +{ + g_sr = 0; + g_cr = 0; +} + +static void teardown(void) +{ +} + +/* The HAL takes a 32-bit address; on a 64-bit host the test buffer must + * live below 4 GiB so the truncating cast is exact. */ +static uint32_t *alloc32(size_t size) +{ + void *p = mmap(NULL, size, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_32BIT, -1, 0); + + ck_assert_ptr_ne(p, MAP_FAILED); + return (uint32_t *)p; +} + +/* Clean status: the write succeeds and the data lands. */ +START_TEST(test_write_success){ + uint32_t *dst = alloc32(8); + + memset(dst, 0, 8); + g_sr = 0; + + ck_assert_int_eq(hal_flash_write((uint32_t)(uintptr_t)dst, + (const uint8_t *)dst, 8), 0); + munmap(dst, 8); +} +END_TEST + +/* A programming fault in the status register must be reported. The stub + * register keeps the bit set (host has no write-1-to-clear semantics), + * so the pre-program clear does not hide it. */ +START_TEST(test_write_reports_progerr) +{ + uint32_t *dst = alloc32(8); + + memset(dst, 0, 8); + g_sr = FLASH_SR_PROGERR; + + ck_assert_int_eq(hal_flash_write((uint32_t)(uintptr_t)dst, + (const uint8_t *)dst, 8), -1); + munmap(dst, 8); +} +END_TEST + +/* A write-protection fault must be reported too. */ +START_TEST(test_write_reports_wrperr) +{ + uint32_t *dst = alloc32(8); + + memset(dst, 0, 8); + g_sr = FLASH_SR_WRPERR; + + ck_assert_int_eq(hal_flash_write((uint32_t)(uintptr_t)dst, + (const uint8_t *)dst, 8), -1); + munmap(dst, 8); +} +END_TEST + +/* The programming unit is a double word: a misaligned address is + * rejected before any flash activity. */ +START_TEST(test_write_misaligned_rejected) +{ + uint32_t *dst = alloc32(8); + + memset(dst, 0, 8); + + ck_assert_int_eq(hal_flash_write((uint32_t)(uintptr_t)(dst + 1), + (const uint8_t *)dst, 8), -1); + ck_assert_uint_eq(g_cr, 0); + munmap(dst, 8); +} +END_TEST + +Suite *stm32u3_flash_write_suite(void) +{ + Suite *s = suite_create("stm32u3-flash-write"); + TCase *tc = tcase_create("stm32u3-flash-write"); + + tcase_add_checked_fixture(tc, setup, teardown); + tcase_add_test(tc, test_write_success); + tcase_add_test(tc, test_write_reports_progerr); + tcase_add_test(tc, test_write_reports_wrperr); + tcase_add_test(tc, test_write_misaligned_rejected); + suite_add_tcase(s, tc); + + return s; +} + +int main(void) +{ + int fails; + Suite *s = stm32u3_flash_write_suite(); + SRunner *sr = srunner_create(s); + + srunner_run_all(sr, CK_NORMAL); + fails = srunner_ntests_failed(sr); + srunner_free(sr); + + return fails; +} From 7860fa6c7acb324264bf053f5752475a3dba3568 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 07:07:07 +0200 Subject: [PATCH 15/50] F-12071: early-exit sector scan for sector-zero ranges The not-found sentinel is -1, so the early-exit test must be start >= 0 && end >= 0; with '> 0' any range involving sector zero scanned the whole table. Behavior-preserving: each address falls in exactly one bucket, so the full scan produced the same result. --- hal/stm32f4.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hal/stm32f4.c b/hal/stm32f4.c index 6bd2bd1c2c..06b445a532 100644 --- a/hal/stm32f4.c +++ b/hal/stm32f4.c @@ -251,7 +251,7 @@ int RAMFUNCTION hal_flash_erase(uint32_t _address, int len) if ((end_address >= flash_sector[i]) && (end_address < flash_sector[i + 1])) { end = i; } - if (start > 0 && end > 0) + if (start >= 0 && end >= 0) break; } if (start < 0 || end < 0) From 9e9cdbef0f6a4dfea462d9c0af2c695d93cb7a22 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 08:51:11 +0200 Subject: [PATCH 16/50] F-12070: validate the persisted slot position in find_object_buffer A corrupted node-table pos was used to compute the object address without a range check, unlike bitmap_put() which rejects positions outside KEYVAULT_MAX_ITEMS. Both storage implementations now delete the node and return NULL when pos is out of range, matching the existing "cannot recover object payload" path. Verification: new test seeds the node table with pos = KEYVAULT_MAX_ITEMS and matching id's in the backup sector, so the unfixed restore path reaches the out-of-range slot (mock flash fails on the erase); fails pre-fix in unit-psa_store and unit-pkcs11_store, 10/10 and 13/13 post-fix; full make -C tools/unit-tests run green. --- src/pkcs11_store.c | 4 +++ src/psa_store.c | 4 +++ tools/unit-tests/unit-pkcs11_store.c | 39 ++++++++++++++++++++++++++++ tools/unit-tests/unit-psa_store.c | 39 ++++++++++++++++++++++++++++ 4 files changed, 86 insertions(+) diff --git a/src/pkcs11_store.c b/src/pkcs11_store.c index 5ad3dde1e7..445f64ba4e 100644 --- a/src/pkcs11_store.c +++ b/src/pkcs11_store.c @@ -514,6 +514,10 @@ static uint8_t *find_object_buffer(int32_t type, uint32_t tok_id, uint32_t obj_i uint32_t in_sector_off = obj_off % WOLFBOOT_SECTOR_SIZE; uint32_t sector_base = obj_off - in_sector_off; + if (hdr->pos >= KEYVAULT_MAX_ITEMS) { + delete_object(type, tok_id, obj_id); + return NULL; /* Corrupted slot position */ + } tok_obj_stored = (uint32_t *)(sector_ptr(sector_base) + in_sector_off); if ((tok_obj_stored[0] != tok_id) || (tok_obj_stored[1] != obj_id)) { diff --git a/src/psa_store.c b/src/psa_store.c index 8f95e965aa..1469b615ea 100644 --- a/src/psa_store.c +++ b/src/psa_store.c @@ -257,6 +257,10 @@ static uint8_t *find_object_buffer(int32_t type, uint32_t tok_id, uint32_t obj_i while ((uintptr_t)hdr < ((uintptr_t)vault_base + WOLFBOOT_SECTOR_SIZE)) { if ((hdr->token_id == tok_id) && (hdr->object_id == obj_id) && (hdr->type == type)) { + if (hdr->pos >= KEYVAULT_MAX_ITEMS) { + delete_object(type, tok_id, obj_id); + return NULL; /* Corrupted slot position */ + } tok_obj_stored = (uint32_t *) (vault_base + (2 * WOLFBOOT_SECTOR_SIZE) + (hdr->pos * KEYVAULT_OBJ_SIZE)); if ((tok_obj_stored[0] != tok_id) || (tok_obj_stored[1] != obj_id)) { /* Id's don't match. Try backup sector. */ diff --git a/tools/unit-tests/unit-pkcs11_store.c b/tools/unit-tests/unit-pkcs11_store.c index 186ffbf990..9bb7d925e2 100644 --- a/tools/unit-tests/unit-pkcs11_store.c +++ b/tools/unit-tests/unit-pkcs11_store.c @@ -433,6 +433,43 @@ START_TEST(test_delete_object_corrupted_pos_no_oob) } END_TEST +/* F-12070: a corrupted 'pos' in the node table must not be used to + * compute the object address. find_object_buffer() must reject it and + * delete the node, not dereference an out-of-range slot. */ +START_TEST(test_find_object_buffer_corrupted_pos_no_oob) +{ + const int32_t type = DYNAMIC_TYPE_RSA; + const uint32_t tok_id = 0x21222324U; + const uint32_t obj_id = 0x60616263U; + struct obj_hdr *hdr; + int ret; + + ret = mmap_file(vault_path, vault_base, + keyvault_size, NULL); + ck_assert_int_eq(ret, 0); + memset(vault_base, 0xFF, keyvault_size); + + ((uint32_t *)vault_base)[0] = VAULT_HEADER_MAGIC; + memset(vault_base + sizeof(uint32_t), 0x00, BITMAP_SIZE); + + hdr = NODES_TABLE; + hdr->token_id = tok_id; + hdr->object_id = obj_id; + hdr->type = type; + hdr->pos = KEYVAULT_MAX_ITEMS; + hdr->size = 2 * sizeof(uint32_t); + + /* Seed the backup sector with the object id's: without the range + * check, the restore path would use the out-of-range slot. */ + ((uint32_t *)BACKUP_SECTOR_ADDRESS)[0] = tok_id; + ((uint32_t *)BACKUP_SECTOR_ADDRESS)[1] = obj_id; + + ck_assert_ptr_null(find_object_buffer(type, tok_id, obj_id)); + ck_assert_uint_eq(NODES_TABLE->token_id, PKCS11_INVALID_ID); + ck_assert_uint_eq(NODES_TABLE->object_id, PKCS11_INVALID_ID); +} +END_TEST + START_TEST(test_find_object_search_stops_at_header_sector) { const int32_t type = DYNAMIC_TYPE_RSA; @@ -951,6 +988,8 @@ Suite *wolfboot_suite(void) tcase_add_test(tcase_close, test_close_clears_handle_state); tcase_add_test(tcase_delete_object, test_delete_object_ignores_metadata_prefix); tcase_add_test(tcase_delete_corrupted, test_delete_object_corrupted_pos_no_oob); + tcase_add_test(tcase_delete_corrupted, + test_find_object_buffer_corrupted_pos_no_oob); tcase_add_test(tcase_find_bounds, test_find_object_search_stops_at_header_sector); tcase_add_test(tcase_remanence, test_shorter_overwrite_erases_residual_key_material); tcase_add_test(tcase_neg_len, test_store_rejects_negative_len); diff --git a/tools/unit-tests/unit-psa_store.c b/tools/unit-tests/unit-psa_store.c index 0823568212..1745e6bc92 100644 --- a/tools/unit-tests/unit-psa_store.c +++ b/tools/unit-tests/unit-psa_store.c @@ -192,6 +192,43 @@ START_TEST(test_delete_object_corrupted_pos_no_oob) } END_TEST +/* F-12070: a corrupted 'pos' in the node table must not be used to + * compute the object address. find_object_buffer() must reject it and + * delete the node, not dereference an out-of-range slot. */ +START_TEST(test_find_object_buffer_corrupted_pos_no_oob) +{ + enum { type = WOLFPSA_STORE_KEY }; + const uint32_t tok_id = 0x21222324U; + const uint32_t obj_id = 0x60616263U; + struct obj_hdr *hdr; + int ret; + + ret = mmap_file("/tmp/wolfboot-unit-psa-keyvault.bin", vault_base, + keyvault_size, NULL); + ck_assert_int_eq(ret, 0); + memset(vault_base, 0xFF, keyvault_size); + + ((uint32_t *)vault_base)[0] = VAULT_HEADER_MAGIC; + memset(vault_base + sizeof(uint32_t), 0x00, BITMAP_SIZE); + + hdr = NODES_TABLE; + hdr->token_id = tok_id; + hdr->object_id = obj_id; + hdr->type = type; + hdr->pos = KEYVAULT_MAX_ITEMS; + hdr->size = 2 * sizeof(uint32_t); + + /* Seed the backup sector with the object id's: without the range + * check, the restore path would use the out-of-range slot. */ + ((uint32_t *)BACKUP_SECTOR_ADDRESS)[0] = tok_id; + ((uint32_t *)BACKUP_SECTOR_ADDRESS)[1] = obj_id; + + ck_assert_ptr_null(find_object_buffer(type, tok_id, obj_id)); + ck_assert_uint_eq(NODES_TABLE->token_id, WOLFPSA_INVALID_ID); + ck_assert_uint_eq(NODES_TABLE->object_id, WOLFPSA_INVALID_ID); +} +END_TEST + START_TEST(test_find_object_search_stops_at_header_sector) { enum { type = WOLFPSA_STORE_KEY }; @@ -438,6 +475,8 @@ Suite *wolfboot_suite(void) tcase_add_test(tcase_close, test_close_clears_handle_state); tcase_add_test(tcase_delete, test_delete_object_ignores_metadata_prefix); tcase_add_test(tcase_delete_corrupted, test_delete_object_corrupted_pos_no_oob); + tcase_add_test(tcase_delete_corrupted, + test_find_object_buffer_corrupted_pos_no_oob); tcase_add_test(tcase_find_bounds, test_find_object_search_stops_at_header_sector); tcase_add_test(tcase_tail, test_shorter_overwrite_clears_tail); tcase_add_test(tcase_zeroize, test_cache_commit_zeroizes_cached_sector); From a2ad3c5c37bc0e29ddc3055c329cc074512cb37c Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 08:59:18 +0200 Subject: [PATCH 17/50] F-13607: bound the whole object header in the store guards The update_store_size()/store_live_size() guards only bounded the first byte of the 32-byte struct obj_hdr (and used '>'), so a header offset in (SECTOR_SIZE-20, SECTOR_SIZE] would write/read the 'size' field into the adjacent cache slot. Compute the offset first and reject anything past SECTOR_SIZE - sizeof(struct obj_hdr), in all three copies (pkcs11 update_store_size, pkcs11 store_live_size, psa update_store_size). New tests forge a header at SECTOR_SIZE-16 and assert the write is rejected without touching the adjacent slot (pkcs11) / without any flash traffic (psa). Full unit suite green. --- src/pkcs11_store.c | 10 ++-- src/psa_store.c | 5 +- tools/unit-tests/unit-pkcs11_store.c | 76 ++++++++++++++++++++++++++++ tools/unit-tests/unit-psa_store.c | 28 ++++++++++ 4 files changed, 110 insertions(+), 9 deletions(-) diff --git a/src/pkcs11_store.c b/src/pkcs11_store.c index 445f64ba4e..275de87b1e 100644 --- a/src/pkcs11_store.c +++ b/src/pkcs11_store.c @@ -621,11 +621,10 @@ static void update_store_size(struct obj_hdr *hdr, uint32_t size) uint8_t *s0; struct obj_hdr *hdr_mem; - if (((uint8_t *)hdr) < vault_base || - ((uint8_t *)hdr > vault_base + WOLFBOOT_SECTOR_SIZE)) { + off = (uint32_t)((uint8_t *)hdr - vault_base); + if (off > WOLFBOOT_SECTOR_SIZE - (uint32_t)sizeof(struct obj_hdr)) { return; } - off = (uintptr_t)hdr - (uintptr_t)vault_base; s0 = cache_get_sector(0); hdr_mem = (struct obj_hdr *)(s0 + off); hdr_mem->size = size; @@ -778,11 +777,10 @@ static uint32_t store_live_size(struct store_handle *handle) { uint32_t off; - if (((uint8_t *)handle->hdr) < vault_base || - ((uint8_t *)handle->hdr > vault_base + WOLFBOOT_SECTOR_SIZE)) { + off = (uint32_t)((uint8_t *)handle->hdr - vault_base); + if (off > WOLFBOOT_SECTOR_SIZE - (uint32_t)sizeof(struct obj_hdr)) { return 0; } - off = (uint32_t)((uintptr_t)handle->hdr - (uintptr_t)vault_base); return ((struct obj_hdr *)(sector0_ptr() + off))->size; } diff --git a/src/psa_store.c b/src/psa_store.c index 1469b615ea..a310472ba7 100644 --- a/src/psa_store.c +++ b/src/psa_store.c @@ -357,11 +357,10 @@ static void update_store_size(struct obj_hdr *hdr, uint32_t size) { uint32_t off; struct obj_hdr *hdr_mem; - if (((uint8_t *)hdr) < vault_base || - ((uint8_t *)hdr > vault_base + WOLFBOOT_SECTOR_SIZE)) + off = (uint32_t)((uint8_t *)hdr - vault_base); + if (off > WOLFBOOT_SECTOR_SIZE - (uint32_t)sizeof(struct obj_hdr)) return; check_vault(); - off = (uintptr_t)hdr - (uintptr_t)vault_base; memcpy(cached_sector, vault_base, WOLFBOOT_SECTOR_SIZE); hdr_mem = (struct obj_hdr *)(cached_sector + off); hdr_mem->size = size; diff --git a/tools/unit-tests/unit-pkcs11_store.c b/tools/unit-tests/unit-pkcs11_store.c index 9bb7d925e2..e03da28511 100644 --- a/tools/unit-tests/unit-pkcs11_store.c +++ b/tools/unit-tests/unit-pkcs11_store.c @@ -470,6 +470,80 @@ START_TEST(test_find_object_buffer_corrupted_pos_no_oob) } END_TEST +/* F-13607: update_store_size() must reject a header pointer whose + * 'size' field (offset 16) lands past the end of the header sector. + * The old guard only bounded the first byte of the 32-byte struct. */ +START_TEST(test_update_store_size_oob_header_rejected) +{ + struct obj_hdr *forged; + uint8_t *s0; + int slot = -1; + int i; + int ret; + + ret = mmap_file(vault_path, vault_base, + keyvault_size, NULL); + ck_assert_int_eq(ret, 0); + memset(vault_base, 0xFF, keyvault_size); + + ((uint32_t *)vault_base)[0] = VAULT_HEADER_MAGIC; + memset(vault_base + sizeof(uint32_t), 0x00, BITMAP_SIZE); + + s0 = cache_get_sector(0); + for (i = 0; i < WOLFBOOT_PKCS11_STORE_CACHE_SECTORS; i++) { + if (store_cache[i].sector == s0) { + slot = i; + break; + } + } + ck_assert_int_ge(slot, 0); + ck_assert_int_lt(slot + 1, WOLFBOOT_PKCS11_STORE_CACHE_SECTORS); + memset(cache_sector_mem[slot + 1], 0, WOLFBOOT_SECTOR_SIZE); + + /* off = SECTOR_SIZE - 16: the 'size' field lands at SECTOR_SIZE, + * the first word of the adjacent cache slot. */ + forged = (struct obj_hdr *)(vault_base + WOLFBOOT_SECTOR_SIZE - 16); + update_store_size(forged, 0x12345678); + ck_assert_uint_eq(((uint32_t *)cache_sector_mem[slot + 1])[0], 0); +} +END_TEST + +/* F-13607: store_live_size() must reject a header pointer past the + * end of the header sector instead of reading the adjacent slot. */ +START_TEST(test_store_live_size_oob_header_rejected) +{ + struct store_handle handle; + uint8_t *s0; + int slot = -1; + int i; + int ret; + + ret = mmap_file(vault_path, vault_base, + keyvault_size, NULL); + ck_assert_int_eq(ret, 0); + memset(vault_base, 0xFF, keyvault_size); + + ((uint32_t *)vault_base)[0] = VAULT_HEADER_MAGIC; + memset(vault_base + sizeof(uint32_t), 0x00, BITMAP_SIZE); + + s0 = cache_get_sector(0); + for (i = 0; i < WOLFBOOT_PKCS11_STORE_CACHE_SECTORS; i++) { + if (store_cache[i].sector == s0) { + slot = i; + break; + } + } + ck_assert_int_ge(slot, 0); + ck_assert_int_lt(slot + 1, WOLFBOOT_PKCS11_STORE_CACHE_SECTORS); + ((uint32_t *)cache_sector_mem[slot + 1])[0] = 0xDEADBEEF; + + memset(&handle, 0, sizeof(handle)); + handle.hdr = (struct obj_hdr *)(vault_base + + WOLFBOOT_SECTOR_SIZE - 16); + ck_assert_uint_eq(store_live_size(&handle), 0); +} +END_TEST + START_TEST(test_find_object_search_stops_at_header_sector) { const int32_t type = DYNAMIC_TYPE_RSA; @@ -991,6 +1065,8 @@ Suite *wolfboot_suite(void) tcase_add_test(tcase_delete_corrupted, test_find_object_buffer_corrupted_pos_no_oob); tcase_add_test(tcase_find_bounds, test_find_object_search_stops_at_header_sector); + tcase_add_test(tcase_find_bounds, test_update_store_size_oob_header_rejected); + tcase_add_test(tcase_find_bounds, test_store_live_size_oob_header_rejected); tcase_add_test(tcase_remanence, test_shorter_overwrite_erases_residual_key_material); tcase_add_test(tcase_neg_len, test_store_rejects_negative_len); tcase_add_test(tcase_remove_erase, test_remove_erases_payload_from_flash); diff --git a/tools/unit-tests/unit-psa_store.c b/tools/unit-tests/unit-psa_store.c index 1745e6bc92..6ae6cf0f00 100644 --- a/tools/unit-tests/unit-psa_store.c +++ b/tools/unit-tests/unit-psa_store.c @@ -229,6 +229,33 @@ START_TEST(test_find_object_buffer_corrupted_pos_no_oob) } END_TEST +/* F-13607: update_store_size() must reject a header pointer whose + * 'size' field (offset 16) lands past the end of the header sector. + * The old guard only bounded the first byte of the 32-byte struct. */ +START_TEST(test_update_store_size_oob_header_rejected) +{ + struct obj_hdr *forged; + int erased_before; + int ret; + + ret = mmap_file("/tmp/wolfboot-unit-psa-keyvault.bin", vault_base, + keyvault_size, NULL); + ck_assert_int_eq(ret, 0); + memset(vault_base, 0xFF, keyvault_size); + + ((uint32_t *)vault_base)[0] = VAULT_HEADER_MAGIC; + memset(vault_base + sizeof(uint32_t), 0x00, BITMAP_SIZE); + + erased_before = erased_vault; + /* off = SECTOR_SIZE - 16: the 'size' field lands at SECTOR_SIZE, + * one word past the header sector. The guard must reject before + * any flash traffic. */ + forged = (struct obj_hdr *)(vault_base + WOLFBOOT_SECTOR_SIZE - 16); + update_store_size(forged, 0x12345678); + ck_assert_int_eq(erased_vault, erased_before); +} +END_TEST + START_TEST(test_find_object_search_stops_at_header_sector) { enum { type = WOLFPSA_STORE_KEY }; @@ -478,6 +505,7 @@ Suite *wolfboot_suite(void) tcase_add_test(tcase_delete_corrupted, test_find_object_buffer_corrupted_pos_no_oob); tcase_add_test(tcase_find_bounds, test_find_object_search_stops_at_header_sector); + tcase_add_test(tcase_find_bounds, test_update_store_size_oob_header_rejected); tcase_add_test(tcase_tail, test_shorter_overwrite_clears_tail); tcase_add_test(tcase_zeroize, test_cache_commit_zeroizes_cached_sector); tcase_add_test(tcase_neg_len, test_store_rejects_negative_len); From d6926a7c1f9946e6e4878abdd727724d14148759 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 09:04:29 +0200 Subject: [PATCH 18/50] F-11030: consume long/size_t/pointer args at full width in uart_vprintf The UART formatter recognized 'l' and 'z' length modifiers and %p, but still consumed the argument as int, truncating long, size_t and pointers on 64-bit targets (undefined behavior). Track the 'z' modifier and, where PRINTF_LONG_LONG is enabled, consume long / unsigned long / void * and format through the 64-bit uart_writenum_ll() path; the 32-bit path is unchanged (long == int width there). New test test_uart_printf_64bit_args covers %ld, %lu, %zu, %p, %lx with values past 32 bits plus %lld and %d regressions; fails pre-fix (truncated output), green post-fix. 32-bit compile of string.c verified with -m32 -Wall -Wextra. --- src/string.c | 47 +++++++++++++++++++++++++++++++--- tools/unit-tests/unit-string.c | 39 ++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/src/string.c b/src/string.c index bf31b5669c..cdbfe317bd 100644 --- a/src/string.c +++ b/src/string.c @@ -449,7 +449,7 @@ static void uart_writenum_ll(unsigned long long val, int is_negative, void uart_vprintf(const char* fmt, va_list argp) { char* fmtp = (char*)fmt; - int zeropad, maxdigits, precision, leftjust, islong; + int zeropad, maxdigits, precision, leftjust, islong, iszl; while (fmtp != NULL && *fmtp != '\0') { /* print non formatting characters */ if (*fmtp != '%') { @@ -459,7 +459,7 @@ void uart_vprintf(const char* fmt, va_list argp) fmtp++; /* skip % */ /* find formatters */ - zeropad = maxdigits = leftjust = islong = 0; + zeropad = maxdigits = leftjust = islong = iszl = 0; precision = -1; /* -1 = not specified */ /* check for left-justify flag */ if (*fmtp == '-') { @@ -500,7 +500,8 @@ void uart_vprintf(const char* fmt, va_list argp) fmtp++; } else if (*fmtp == 'z') { - /* auto type - skip */ + /* size_t - consume as long */ + iszl = 1; fmtp++; } else { @@ -538,6 +539,26 @@ void uart_vprintf(const char* fmt, va_list argp) } uart_writenum_ll(val, is_neg, 10, zeropad, maxdigits); } + else if (islong == 1 || iszl) { + /* %ld / %lu / %zd / %zu: long is 64-bit here */ + int is_neg = 0; + unsigned long long val; + if (*fmtp != 'u') { + long sl = va_arg(argp, long); + if (sl < 0) { + is_neg = 1; + val = 0ULL - (unsigned long long)sl; + } + else { + val = (unsigned long long)sl; + } + } + else { + val = (unsigned long long)va_arg(argp, + unsigned long); + } + uart_writenum_ll(val, is_neg, 10, zeropad, maxdigits); + } else #endif { @@ -559,10 +580,28 @@ void uart_vprintf(const char* fmt, va_list argp) va_arg(argp, unsigned long long); uart_writenum_ll(val, 0, 16, zeropad, maxdigits); } + else if (islong == 1 || iszl || *fmtp == 'p') { + /* %lx / %zx / %p: long/pointer is 64-bit here */ + unsigned long long val; + if (*fmtp == 'p') { + val = (unsigned long long)va_arg(argp, void *); + } + else { + val = (unsigned long long)va_arg(argp, + unsigned long); + } + uart_writenum_ll(val, 0, 16, zeropad, maxdigits); + } else #endif { - int n = (int)va_arg(argp, int); + int n; + if (*fmtp == 'p') { + n = (int)(uintptr_t)va_arg(argp, void *); + } + else { + n = (int)va_arg(argp, int); + } uart_writenum(n, 16, zeropad, maxdigits); } break; diff --git a/tools/unit-tests/unit-string.c b/tools/unit-tests/unit-string.c index 3ca21f3dda..fb1e9e255a 100644 --- a/tools/unit-tests/unit-string.c +++ b/tools/unit-tests/unit-string.c @@ -454,6 +454,44 @@ START_TEST(test_uart_printf_formats) } END_TEST +/* F-11030: %ld/%lu/%zd/%zu/%lx/%p must consume the full-width + * argument, not int (truncation on 64-bit hosts). */ +START_TEST(test_uart_printf_64bit_args) +{ + reset_uart_buf(); + uart_printf("%ld", (long)1234567890123LL); + ck_assert_str_eq(uart_buf, "1234567890123"); + + reset_uart_buf(); + uart_printf("%lu", (unsigned long)18446744073709551615ULL); + ck_assert_str_eq(uart_buf, "18446744073709551615"); + + reset_uart_buf(); + uart_printf("%zu", (size_t)4294967296ULL); + ck_assert_str_eq(uart_buf, "4294967296"); + + reset_uart_buf(); + uart_printf("%p", (void*)(uintptr_t)0x1234567890ULL); + ck_assert_str_eq(uart_buf, "0x1234567890"); + + reset_uart_buf(); + uart_printf("%lx", (unsigned long)0xABCDEF0123ULL); + ck_assert_str_eq(uart_buf, "ABCDEF0123"); + + reset_uart_buf(); + uart_printf("%ld", (long)-1234567890123LL); + ck_assert_str_eq(uart_buf, "-1234567890123"); + + reset_uart_buf(); + uart_printf("%lld", (long long)-1234567890123LL); + ck_assert_str_eq(uart_buf, "-1234567890123"); + + reset_uart_buf(); + uart_printf("%d", 42); + ck_assert_str_eq(uart_buf, "42"); +} +END_TEST + Suite *string_suite(void) { Suite *s = suite_create("String"); @@ -488,6 +526,7 @@ Suite *string_suite(void) tcase_add_test(tcase_misc, test_memcpy_aligned_buffers); tcase_add_test(tcase_misc, test_uart_writenum_basic); tcase_add_test(tcase_misc, test_uart_printf_formats); + tcase_add_test(tcase_misc, test_uart_printf_64bit_args); suite_add_tcase(s, tcase_strncasecmp); suite_add_tcase(s, tcase_misc); From 0702a6123c5ecdfc167c7f5efac40bdb45198722 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 09:06:32 +0200 Subject: [PATCH 19/50] F-11048: clamp negative '*' field widths in uart_vprintf A negative '*' width passed the upper-bound-only clamp and reached the zero-pad memset as a huge size_t (stack overflow). Clamp to zero at the parser, the single source of dynamic widths; the existing zero-to-8 default then applies. New test test_uart_printf_negative_star_width covers %0*x and %0*llu; SIGSEGV pre-fix, green post-fix. Full unit suite green. --- src/string.c | 5 +++++ tools/unit-tests/unit-string.c | 16 ++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/string.c b/src/string.c index cdbfe317bd..ac715fb9f7 100644 --- a/src/string.c +++ b/src/string.c @@ -470,6 +470,11 @@ void uart_vprintf(const char* fmt, va_list argp) if (*fmtp == '*') { /* width from argument */ maxdigits = va_arg(argp, int); + if (maxdigits < 0) { + /* F-11048: a negative width would become a huge + * size_t in the zero-pad memset below. */ + maxdigits = 0; + } fmtp++; } else if (*fmtp >= '0' && *fmtp <= '9') { diff --git a/tools/unit-tests/unit-string.c b/tools/unit-tests/unit-string.c index fb1e9e255a..0f32d63adc 100644 --- a/tools/unit-tests/unit-string.c +++ b/tools/unit-tests/unit-string.c @@ -492,6 +492,21 @@ START_TEST(test_uart_printf_64bit_args) } END_TEST +/* F-11048: a negative '*' width must not reach the zero-pad memset + * as a huge size_t. */ +START_TEST(test_uart_printf_negative_star_width) +{ + reset_uart_buf(); + uart_printf("%0*x", -3, 0x2a); + /* clamped to 0 -> default 8-digit zero pad */ + ck_assert_str_eq(uart_buf, "0000002A"); + + reset_uart_buf(); + uart_printf("%0*llu", -1, 0x123ULL); + ck_assert_str_eq(uart_buf, "00000291"); +} +END_TEST + Suite *string_suite(void) { Suite *s = suite_create("String"); @@ -527,6 +542,7 @@ Suite *string_suite(void) tcase_add_test(tcase_misc, test_uart_writenum_basic); tcase_add_test(tcase_misc, test_uart_printf_formats); tcase_add_test(tcase_misc, test_uart_printf_64bit_args); + tcase_add_test(tcase_misc, test_uart_printf_negative_star_width); suite_add_tcase(s, tcase_strncasecmp); suite_add_tcase(s, tcase_misc); From 647022b1830bf4d4ccee00430131aa4e506dc1d4 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 09:11:56 +0200 Subject: [PATCH 20/50] x86: derive UART stop-bit field from requested stop count (F-12885) uart_init() initialized a local to zero and tested that local, so the LCR stop-bit field was always zero (one stop bit) and stop values other than 1/2 were silently accepted. Validate stop against 1/2 and derive the field from the parameter. Add unit-x86-uart (linked real driver, io_write8/io_read8 mocked) covering 8N1/8N2/5O2 LCR encodings plus stop-count and existing argument rejection; 3 of 5 tests fail pre-fix. --- hal/x86_uart.c | 12 ++- tools/unit-tests/Makefile | 6 ++ tools/unit-tests/unit-x86-uart.c | 135 +++++++++++++++++++++++++++++++ 3 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 tools/unit-tests/unit-x86-uart.c diff --git a/hal/x86_uart.c b/hal/x86_uart.c index 70749d276c..11f7fbfbb6 100644 --- a/hal/x86_uart.c +++ b/hal/x86_uart.c @@ -127,12 +127,18 @@ int uart_init(uint32_t bitrate, uint8_t data, char parity, uint8_t stop) return -1; } + if (stop != 1 && stop != 2) { + return -1; + } + stops = 0; - if (stops > 1) - stops = 0x01; + if (stop == 2) { + stops = 1; + } - if (bitrate == 0) + if (bitrate == 0) { return -1; + } divisor = 115200 / bitrate; write_reg(X86_UART_LCR, ENABLE_DLA); diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index 469ee1b390..52eb2c2d01 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -108,6 +108,7 @@ TESTS+=unit-elf-bss-guard TESTS+=unit-elf-entry-inplace TESTS+=unit-elf-mmu-fail TESTS+=unit-nrf54l-uart-write +TESTS+=unit-x86-uart TESTS+=unit-stm32h7-flash-write TESTS+=unit-stm32u3-flash-write TESTS+=unit-elf-phentsize @@ -1399,6 +1400,11 @@ nrf54l_uart_write_extract.h: ../../hal/nrf54l.c unit-nrf54l-uart-write: unit-nrf54l-uart-write.c nrf54l_uart_write_extract.h gcc -o $@ unit-nrf54l-uart-write.c $(CFLAGS) $(LDFLAGS) +# unit-x86-uart: uart_init() must derive the stop-bit field from the +# requested stop count (F-12885); port I/O is mocked. +unit-x86-uart: unit-x86-uart.c ../../hal/x86_uart.c + gcc -o $@ unit-x86-uart.c ../../hal/x86_uart.c $(CFLAGS) $(LDFLAGS) + # unit-stm32h7-flash-write: the bank-boundary dispatcher splits cross-bank # requests (F-12871); hal_flash_write_part is mocked. stm32h7_flash_write_extract.h: ../../hal/stm32h7.c diff --git a/tools/unit-tests/unit-x86-uart.c b/tools/unit-tests/unit-x86-uart.c new file mode 100644 index 0000000000..9f367d3634 --- /dev/null +++ b/tools/unit-tests/unit-x86-uart.c @@ -0,0 +1,135 @@ +/* unit-x86-uart.c + * + * Regression test for F-12885: uart_init() in hal/x86_uart.c derived the + * stop-bit field from an always-zero local variable instead of the + * requested stop count, so the UART was always configured for one stop + * bit (and stop values other than 1/2 were silently accepted). + * + * The real driver is linked in; io_write8()/io_read8() are recording + * mocks standing in for the port I/O primitives. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#include +#include +#include +#include + +#include + +/* Default port base in hal/x86_uart.c; LCR is register 3. */ +#define TEST_LCR_PORT (0x3f8 + 3) + +static uint8_t last_lcr; +static int lcr_writes; + +void io_write8(uint16_t port, uint8_t value) +{ + if (port == TEST_LCR_PORT) { + last_lcr = value; + lcr_writes++; + } +} + +uint8_t io_read8(uint16_t port) +{ + (void)port; + return 0x20; +} + +static void setup(void) +{ + last_lcr = 0; + lcr_writes = 0; +} + +static void teardown(void) +{ +} + +/* 115200 8-N-1: LCR = DATA_8_BIT (0x03). */ +START_TEST(test_uart_init_one_stop_bit) +{ + ck_assert_int_eq(uart_init(115200, 8, 'N', 1), 0); + ck_assert_int_gt(lcr_writes, 0); + ck_assert_uint_eq(last_lcr, 0x03); +} +END_TEST + +/* 115200 8-N-2: LCR = DATA_8_BIT | (1 << 2) = 0x07. Pre-fix the + * stop-bit field was always zero and the LCR came out 0x03. */ +START_TEST(test_uart_init_two_stop_bits) +{ + ck_assert_int_eq(uart_init(115200, 8, 'N', 2), 0); + ck_assert_int_gt(lcr_writes, 0); + ck_assert_uint_eq(last_lcr, 0x07); +} +END_TEST + +/* 115200 5-O-2: LCR = DATA_5_BIT | (1 << 2) | (PARITY_ODD << 3) = 0x0C. */ +START_TEST(test_uart_init_parity_and_stops) +{ + ck_assert_int_eq(uart_init(115200, 5, 'O', 2), 0); + ck_assert_int_gt(lcr_writes, 0); + ck_assert_uint_eq(last_lcr, 0x0C); +} +END_TEST + +/* Stop counts other than 1 and 2 are not representable in the LCR. */ +START_TEST(test_uart_init_bad_stop_count) +{ + ck_assert_int_eq(uart_init(115200, 8, 'N', 0), -1); + ck_assert_int_eq(uart_init(115200, 8, 'N', 3), -1); + ck_assert_int_eq(lcr_writes, 0); +} +END_TEST + +/* Existing rejections must be untouched. */ +START_TEST(test_uart_init_bad_args) +{ + ck_assert_int_eq(uart_init(0, 8, 'N', 1), -1); + ck_assert_int_eq(uart_init(115200, 9, 'N', 1), -1); + ck_assert_int_eq(uart_init(115200, 8, 'X', 1), -1); + ck_assert_int_eq(lcr_writes, 0); +} +END_TEST + +int main(void) +{ + SRunner *sr; + Suite *s = suite_create("x86_uart"); + TCase *tc = tcase_create("uart_init"); + int failures; + + tcase_add_unchecked_fixture(tc, setup, teardown); + tcase_add_test(tc, test_uart_init_one_stop_bit); + tcase_add_test(tc, test_uart_init_two_stop_bits); + tcase_add_test(tc, test_uart_init_parity_and_stops); + tcase_add_test(tc, test_uart_init_bad_stop_count); + tcase_add_test(tc, test_uart_init_bad_args); + suite_add_tcase(s, tc); + + sr = srunner_create(s); + srunner_run_all(sr, CK_NORMAL); + failures = srunner_ntests_failed(sr); + srunner_free(sr); + + return (failures == 0) ? EXIT_SUCCESS : EXIT_FAILURE; +} From e6824ae2216f89770c6d60cead50db23b7637a38 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 09:17:54 +0200 Subject: [PATCH 21/50] ramboot: require full payload size from ext_flash_read (F-13605) - ext_flash_read() backends return the number of bytes read; the ramboot payload copy only rejected negative returns, so a positive short read left a truncated image in the RAM load region (executed when WOLFBOOT_SKIP_BOOT_VERIFY is set) - require ret == img_size, mirroring the header read in the same function and the wolfBoot_start() copy (F-12065 fix) - unit test: mock short read on the payload copy aborts ramboot --- src/update_ram.c | 8 ++++++-- tools/unit-tests/unit-update-ram.c | 30 ++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/update_ram.c b/src/update_ram.c index 3b935fe476..f6212f5970 100644 --- a/src/update_ram.c +++ b/src/update_ram.c @@ -164,8 +164,12 @@ int wolfBoot_ramboot(struct wolfBoot_image *img, uint8_t *src, uint8_t *dst) #if defined(EXT_FLASH) && defined(NO_XIP) ret = ext_flash_read((uintptr_t)src + IMAGE_HEADER_SIZE, dst + IMAGE_HEADER_SIZE, img_size); - if (ret < 0) { - wolfBoot_printf("Error reading image at %p\n", src); + /* Backends return the number of bytes read: a positive short read + * leaves a truncated image in the RAM load region, so require the + * full size. Check the signed error range before the unsigned + * comparison. */ + if (ret < 0 || (uint32_t)ret != img_size) { + wolfBoot_printf("Error reading image at %p (ret %d)\n", src, ret); return -1; } #else diff --git a/tools/unit-tests/unit-update-ram.c b/tools/unit-tests/unit-update-ram.c index 62ad476a4a..8b65ace0b9 100644 --- a/tools/unit-tests/unit-update-ram.c +++ b/tools/unit-tests/unit-update-ram.c @@ -287,6 +287,32 @@ START_TEST (test_ramboot_success) } END_TEST +START_TEST (test_ramboot_short_read_rejected) +{ + struct wolfBoot_image img; + int ret; + + reset_mock_stats(); + prepare_flash(); + add_payload(PART_BOOT, 1, TEST_SIZE_SMALL); + mock_ext_flash_short_len = TEST_SIZE_SMALL; + mock_ext_flash_short_bytes = 1; + + memset(&img, 0, sizeof(img)); + ret = wolfBoot_ramboot(&img, + (uint8_t *)WOLFBOOT_PARTITION_BOOT_ADDRESS, wolfboot_ram); + + /* Clear the short-read mock before asserting: the suite runs CK_NOFORK, + * so a failing ck_assert longjmps past any cleanup below and would leave + * every full-size ext_flash_read truncated for the next test. */ + mock_ext_flash_short_len = 0; + mock_ext_flash_short_bytes = 0; + ck_assert_int_eq(ret, -1); + ck_assert_int_eq(img.not_ext, 0); + cleanup_flash(); +} +END_TEST + START_TEST (test_ramboot_overlap_predicate) { /* wolfBoot occupies [0x1000, 0x2000) for these checks (the two-sided @@ -606,6 +632,7 @@ Suite *wolfboot_suite(void) TCase *ramboot_invalid_header = tcase_create("Ramboot invalid header"); TCase *ramboot_oversize = tcase_create("Ramboot oversize"); TCase *ramboot_success = tcase_create("Ramboot success"); + TCase *ramboot_short_read = tcase_create("Ramboot short read"); TCase *ramboot_overlap = tcase_create("Ramboot overlap predicate"); TCase *sunnyday_noupdate = tcase_create("Sunny day test with no update available"); @@ -636,6 +663,7 @@ Suite *wolfboot_suite(void) tcase_add_test(ramboot_invalid_header, test_ramboot_invalid_header); tcase_add_test(ramboot_oversize, test_ramboot_oversize_rejected); tcase_add_test(ramboot_success, test_ramboot_success); + tcase_add_test(ramboot_short_read, test_ramboot_short_read_rejected); tcase_add_test(ramboot_overlap, test_ramboot_overlap_predicate); tcase_add_test(sunnyday_noupdate, test_sunnyday_noupdate); tcase_add_test(forward_update_samesize, test_forward_update_samesize); @@ -660,6 +688,7 @@ Suite *wolfboot_suite(void) suite_add_tcase(s, ramboot_invalid_header); suite_add_tcase(s, ramboot_oversize); suite_add_tcase(s, ramboot_success); + suite_add_tcase(s, ramboot_short_read); suite_add_tcase(s, ramboot_overlap); suite_add_tcase(s, sunnyday_noupdate); suite_add_tcase(s, forward_update_samesize); @@ -683,6 +712,7 @@ Suite *wolfboot_suite(void) tcase_set_timeout(ramboot_invalid_header, 5); tcase_set_timeout(ramboot_oversize, 5); tcase_set_timeout(ramboot_success, 5); + tcase_set_timeout(ramboot_short_read, 5); tcase_set_timeout(ramboot_overlap, 5); tcase_set_timeout(sunnyday_noupdate, 5); tcase_set_timeout(forward_update_samesize, 5); From 87b89a052994cd7e782bac75f8d8c983a711209a Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 10:16:28 +0200 Subject: [PATCH 22/50] ramboot: accept image versions with the high bit set (F-13606) wolfBoot_get_blob_version() returns uint32_t, but the value was assigned to 'int ret' and rejected when 'ret <= 0'. Versions with bit 31 set (e.g. 0x80000001) are valid unsigned versions and were silently rejected with 'No valid image found'. - check the version as uint32_t, rejecting only version 0 - add test_ramboot_high_bit_version (version 0x80000001 ramboots) --- src/update_ram.c | 5 +++-- tools/unit-tests/unit-update-ram.c | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/update_ram.c b/src/update_ram.c index f6212f5970..a730acbbc8 100644 --- a/src/update_ram.c +++ b/src/update_ram.c @@ -85,6 +85,7 @@ int wolfBoot_ramboot(struct wolfBoot_image *img, uint8_t *src, uint8_t *dst) { int ret; uint32_t img_size; + uint32_t version; BENCHMARK_DECLARE(); /* read header into RAM */ @@ -101,8 +102,8 @@ int wolfBoot_ramboot(struct wolfBoot_image *img, uint8_t *src, uint8_t *dst) #endif /* check for valid header and version */ - ret = wolfBoot_get_blob_version((uint8_t*)dst); - if (ret <= 0) { + version = wolfBoot_get_blob_version((uint8_t*)dst); + if (version == 0) { wolfBoot_printf("No valid image found at %p\n", src); return -1; } diff --git a/tools/unit-tests/unit-update-ram.c b/tools/unit-tests/unit-update-ram.c index 8b65ace0b9..65df44cb09 100644 --- a/tools/unit-tests/unit-update-ram.c +++ b/tools/unit-tests/unit-update-ram.c @@ -287,6 +287,26 @@ START_TEST (test_ramboot_success) } END_TEST +/* F-13606: the version is a full 32-bit value; a version with the + * high bit set must not be rejected by a signed int comparison. */ +START_TEST (test_ramboot_high_bit_version) +{ + struct wolfBoot_image img; + int ret; + + reset_mock_stats(); + prepare_flash(); + add_payload(PART_BOOT, 0x80000001, TEST_SIZE_SMALL); + + memset(&img, 0, sizeof(img)); + ret = wolfBoot_ramboot(&img, + (uint8_t *)WOLFBOOT_PARTITION_BOOT_ADDRESS, wolfboot_ram); + ck_assert_int_eq(ret, 0); + ck_assert_int_eq(img.not_ext, 1); + cleanup_flash(); +} +END_TEST + START_TEST (test_ramboot_short_read_rejected) { struct wolfBoot_image img; @@ -632,6 +652,7 @@ Suite *wolfboot_suite(void) TCase *ramboot_invalid_header = tcase_create("Ramboot invalid header"); TCase *ramboot_oversize = tcase_create("Ramboot oversize"); TCase *ramboot_success = tcase_create("Ramboot success"); + TCase *ramboot_high_bit = tcase_create("Ramboot high-bit version"); TCase *ramboot_short_read = tcase_create("Ramboot short read"); TCase *ramboot_overlap = tcase_create("Ramboot overlap predicate"); TCase *sunnyday_noupdate = @@ -663,6 +684,7 @@ Suite *wolfboot_suite(void) tcase_add_test(ramboot_invalid_header, test_ramboot_invalid_header); tcase_add_test(ramboot_oversize, test_ramboot_oversize_rejected); tcase_add_test(ramboot_success, test_ramboot_success); + tcase_add_test(ramboot_high_bit, test_ramboot_high_bit_version); tcase_add_test(ramboot_short_read, test_ramboot_short_read_rejected); tcase_add_test(ramboot_overlap, test_ramboot_overlap_predicate); tcase_add_test(sunnyday_noupdate, test_sunnyday_noupdate); @@ -688,6 +710,7 @@ Suite *wolfboot_suite(void) suite_add_tcase(s, ramboot_invalid_header); suite_add_tcase(s, ramboot_oversize); suite_add_tcase(s, ramboot_success); + suite_add_tcase(s, ramboot_high_bit); suite_add_tcase(s, ramboot_short_read); suite_add_tcase(s, ramboot_overlap); suite_add_tcase(s, sunnyday_noupdate); From d6464f96f9aee8d77aadf24bcfebb6e8c96c2c26 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 10:16:34 +0200 Subject: [PATCH 23/50] hifive1: mask the HFROSCCFG divider shift with the divider field (F-9743) HFROSCCFG_DIV_SHIFT(d) masked the shifted value with HFROSCCFG_TRIM (bits 16-23) instead of HFROSCCFG_DIV (bits 0-3), so the divider always programmed zero. - mask with HFROSCCFG_DIV - add unit-hifive1-hfrosccfg pinning the field macros (value, overlap, and the composed init register value) --- hal/hifive1.c | 2 +- tools/unit-tests/Makefile | 10 +++ tools/unit-tests/unit-hifive1-hfrosccfg.c | 104 ++++++++++++++++++++++ 3 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 tools/unit-tests/unit-hifive1-hfrosccfg.c diff --git a/hal/hifive1.c b/hal/hifive1.c index a40e670626..118b8a955c 100644 --- a/hal/hifive1.c +++ b/hal/hifive1.c @@ -137,7 +137,7 @@ #define HFROSCCFG_TRIM 0x001F0000UL #define HFROSCCFG_EN (1UL << 30UL) #define HFROSCCFG_READY (1UL << 31UL) -#define HFROSCCFG_DIV_SHIFT(d) ((d << 0) & HFROSCCFG_TRIM) +#define HFROSCCFG_DIV_SHIFT(d) ((d << 0) & HFROSCCFG_DIV) #define HFROSCCFG_TRIM_SHIFT(t) ((t << 16) & HFROSCCFG_TRIM) #define HFXOSCCFG_EN (1 << 30) diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index 52eb2c2d01..9d3e6e03cf 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -136,6 +136,7 @@ TESTS+=unit-nrf5340-flash-protect TESTS+=unit-keygen-keystore TESTS+=unit-samr21-erase-advance TESTS+=unit-hifive1-flash-write +TESTS+=unit-hifive1-hfrosccfg TESTS+=unit-rp2350-flash-write TESTS+=unit-fwtpm-rsp-overrun TESTS+=unit-fwtpm-cmd-toctou @@ -1614,6 +1615,15 @@ hifive1_flash_write_extract.h: ../../hal/hifive1.c unit-hifive1-flash-write: unit-hifive1-flash-write.c hifive1_flash_write_extract.h gcc -o $@ unit-hifive1-flash-write.c $(CFLAGS) $(LDFLAGS) +# unit-hifive1-hfrosccfg pins the HFROSCCFG field macros extracted from +# hal/hifive1.c (F-9743: the divider field was masked with the trim +# mask, so the divider always programmed zero). +hifive1_hfrosccfg_extract.h: ../../hal/hifive1.c + sed -n '/^#define HFROSCCFG_DIV 0x0000001FUL/,/^#define HFROSCCFG_TRIM_SHIFT(t)/p' $< > $@ + +unit-hifive1-hfrosccfg: unit-hifive1-hfrosccfg.c hifive1_hfrosccfg_extract.h + gcc -o $@ unit-hifive1-hfrosccfg.c $(CFLAGS) $(LDFLAGS) + # unit-rp2350-flash-write runs the real hal_flash_write() from # hal/rp2350.c against a mock flash_range_program() that enforces the # ROM contract (F-12061: unaligned / non page-multiple writes from diff --git a/tools/unit-tests/unit-hifive1-hfrosccfg.c b/tools/unit-tests/unit-hifive1-hfrosccfg.c new file mode 100644 index 0000000000..fc10db33fc --- /dev/null +++ b/tools/unit-tests/unit-hifive1-hfrosccfg.c @@ -0,0 +1,104 @@ +/* unit-hifive1-hfrosccfg.c + * + * Regression test for F-9743: HFROSCCFG_DIV_SHIFT() in hal/hifive1.c + * masked the shifted divider with HFROSCCFG_TRIM (0x001F0000) instead + * of HFROSCCFG_DIV (0x0000001F), so the divider field of the composed + * HFROSCCFG register value was always zero and hifive1_init() + * programmed divider 0 instead of the requested 4. + * + * The real macro definitions are extracted by the Makefile from + * hal/hifive1.c; the test pins the composed register value hifive1_init + * programs. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#include +#include +#include + +#include "hifive1_hfrosccfg_extract.h" + +/* The divider field must carry the requested value unchanged. */ +START_TEST(test_div_shift_carries_value) +{ + int d; + + for (d = 0; d <= 31; d++) { + ck_assert_uint_eq(HFROSCCFG_DIV_SHIFT(d), (uint32_t)d); + } +} +END_TEST + +/* The trim field must land in bits 16..20 and nowhere else. */ +START_TEST(test_trim_shift_carries_value) +{ + int t; + + for (t = 0; t <= 31; t++) { + ck_assert_uint_eq(HFROSCCFG_TRIM_SHIFT(t), + (uint32_t)t << 16); + } +} +END_TEST + +/* The two fields must not overlap: composing a full-range divider and + * trim must keep both values intact. */ +START_TEST(test_fields_do_not_overlap) +{ + uint32_t composed; + + composed = HFROSCCFG_DIV_SHIFT(0x1F) | HFROSCCFG_TRIM_SHIFT(0x1F); + ck_assert_uint_eq(composed, 0x001F001FUL); +} +END_TEST + +/* The exact value hifive1_init() programs: EN | DIV(4) | TRIM(0x10). + * Pre-fix the divider field came out zero (0x40100000). */ +START_TEST(test_hifive1_init_composed_value) +{ + uint32_t reg; + + reg = (HFROSCCFG_EN | + HFROSCCFG_DIV_SHIFT(0x4) | + HFROSCCFG_TRIM_SHIFT(0x10)); + ck_assert_uint_eq(reg, 0x40100004UL); +} +END_TEST + +int main(void) +{ + SRunner *sr; + Suite *s = suite_create("hifive1_hfrosccfg"); + TCase *tc = tcase_create("hfrosccfg"); + int failures; + + tcase_add_test(tc, test_div_shift_carries_value); + tcase_add_test(tc, test_trim_shift_carries_value); + tcase_add_test(tc, test_fields_do_not_overlap); + tcase_add_test(tc, test_hifive1_init_composed_value); + suite_add_tcase(s, tc); + + sr = srunner_create(s); + srunner_run_all(sr, CK_NORMAL); + failures = srunner_ntests_failed(sr); + srunner_free(sr); + + return (failures == 0) ? EXIT_SUCCESS : EXIT_FAILURE; +} From 83d1e2529d9ef9b527895b033f28c2c67c366bf1 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 10:32:31 +0200 Subject: [PATCH 24/50] cmake: warn on security-weakening flags like the Makefile (F-6760) SIGN=NONE, ALLOW_DOWNGRADE and DISABLE_BACKUP were applied silently in the CMake build path while options.mk prints a prominent warning for each. Surface the same wording via message(WARNING) so both supported build systems alarm identically. --- CMakeLists.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1628e75d94..dddb367be8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -794,7 +794,7 @@ endif() #--------------------------------------------------------------------------------------------- if(SIGN STREQUAL "NONE") list(APPEND KEYTOOL_OPTIONS --no-sign) - message(STATUS "Image signing disabled") + message(WARNING "SIGN=NONE / WOLFBOOT_NO_SIGN=1 disables firmware signature verification; images are NOT authenticated. Do not use in production.") set(WOLFBOOT_SIGNING_PRIVATE_KEY "" CACHE INTERNAL "") @@ -1023,6 +1023,7 @@ if(EXT_FLASH) endif() if(ALLOW_DOWNGRADE) + message(WARNING "ALLOW_DOWNGRADE=1 disables anti-rollback enforcement; signed older firmware images can replace newer ones") list(APPEND WOLFBOOT_DEFS ALLOW_DOWNGRADE) endif() @@ -1031,6 +1032,7 @@ if(NVM_FLASH_WRITEONCE) endif() if(DISABLE_BACKUP) + message(WARNING "DISABLE_BACKUP=1 disables power-fail-safe updates; losing power during an update can leave BOOT partially written and unrecoverable") list(APPEND WOLFBOOT_DEFS DISABLE_BACKUP) endif() From 772f640542de50c361e9e13b5aea5db85d77cf6f Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 10:40:20 +0200 Subject: [PATCH 25/50] F-6871: bound ext_flash read/write to the SPI NOR device size - ext_flash_read/ext_flash_write forwarded unvalidated (address, len) to the SDK flash_stream_* primitives; a request past the 16 MB device end reached the driver unchecked, unlike the overflow guard already in ext_flash_erase. - Add ext_flash_in_layout(): reject negative len, base at/past the 16 MB device end, and any end offset beyond it (the base check keeps the size subtraction from wrapping). - Verified: host -fsyntax-only of the bare backend compiles clean; make -C tools/unit-tests run passes. --- hal/rtl8735b.c | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/hal/rtl8735b.c b/hal/rtl8735b.c index b3b5f1d09c..b85cf49543 100644 --- a/hal/rtl8735b.c +++ b/hal/rtl8735b.c @@ -433,9 +433,28 @@ void ext_flash_lock(void) #endif } -int ext_flash_read(uintptr_t address, uint8_t *data, int len) +/* Physical SPI NOR capacity (16 MB) -- the ext_flash_* window. */ +#define RTL8735B_EXT_FLASH_SIZE 0x1000000 + +/* All ext_flash callers derive (address, len) from the partition layout in + * target.h; reject anything past the device end before it reaches the SDK. */ +static int ext_flash_in_layout(uintptr_t address, int len) { if (len < 0) { + return 0; + } + if ((uint32_t)address >= RTL8735B_EXT_FLASH_SIZE) { + return 0; + } + if ((uint32_t)len > RTL8735B_EXT_FLASH_SIZE - (uint32_t)address) { + return 0; + } + return 1; +} + +int ext_flash_read(uintptr_t address, uint8_t *data, int len) +{ + if (!ext_flash_in_layout(address, len)) { return -1; } if (len == 0) { @@ -459,7 +478,7 @@ int ext_flash_read(uintptr_t address, uint8_t *data, int len) int ext_flash_write(uintptr_t address, const uint8_t *data, int len) { - if (len < 0) { + if (!ext_flash_in_layout(address, len)) { return -1; } if (len == 0) { From a5e5d37740e6edc139aec60e58f6b62c385e63a0 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 10:42:08 +0200 Subject: [PATCH 26/50] F-6872: assert hal_bl_scratch covers the bootloader slot budget - The seven RAM start-table scratch slots sit at hard-coded 256-byte strides (last one 512 bytes); nothing checked the 2048-byte buffer still covers them if the layout or the buffer is edited. - Add a _Static_assert on the slot budget plus a comment recording the per-slot contract and the sizeof() re-derivation owed when SPIC adaptor reuse lands (SDK struct sizes are only visible in the SDK backend, so full sizeof-based slots are deferred to that toolchain). - Verified: host -fsyntax-only clean at 2048; shrinking the buffer to 1024 fails with the assertion message; make -C tools/unit-tests run passes. --- hal/rtl8735b.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/hal/rtl8735b.c b/hal/rtl8735b.c index b85cf49543..c7f0428aa9 100644 --- a/hal/rtl8735b.c +++ b/hal/rtl8735b.c @@ -104,6 +104,14 @@ extern void wolfboot_ram_entry(void); static uint8_t hal_bl_scratch[2048] __attribute__((section(".ram.noinit"), aligned(32))); +/* The bootloader writes seven fixed slots into hal_bl_scratch at 256-byte + * strides; the last one (phal_spic_adaptor) gets the remaining 512 bytes. + * The SDK struct sizes are only known in the SDK backend -- when SPIC + * adaptor reuse is enabled, re-derive the slots from sizeof() of the real + * structs and keep this budget check. */ +_Static_assert(sizeof(hal_bl_scratch) >= 6 * 256 + 512, + "hal_bl_scratch must cover the seven bootloader slots"); + /* Exactly the 10 bytes "AmebaPro2\xff" (the trailing 0xff matters; a NUL pad * fails as "Invalid FW Image Signature"). */ const unsigned char hal_ram_img_sig[10] From 6da35c8c2eef398e06b9497a2b069ae676b9917e Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 10:48:08 +0200 Subject: [PATCH 27/50] F-9741: close fd on mmap failure in sim HAL mmap_file() left the file descriptor open when mmap() failed; the current callers exit(-1) so the leak was invisible, but close it on the error path. --- hal/sim.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hal/sim.c b/hal/sim.c index ee0395efba..63725aecba 100644 --- a/hal/sim.c +++ b/hal/sim.c @@ -343,8 +343,10 @@ static int mmap_file(const char *path, uint8_t *address, uint8_t** ret_address) mmaped_addr = mmap(address, st.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); - if (mmaped_addr == MAP_FAILED) + if (mmaped_addr == MAP_FAILED) { + close(fd); return -1; + } wolfBoot_printf( "Simulator assigned %s to base %p\n", path, mmaped_addr); From 93da3768d27c0a89d94ba354221c989117298de9 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 10:52:19 +0200 Subject: [PATCH 28/50] F-9742: reject cross-bank flash erase on TI Hercules hal_flash_erase() selected the bank geometry from the start address only; a range extending past the end of that bank was partially erased and reported as success. Derive the bank end from the sector geometry and reject cross-bank requests before erasing anything. New unit test extracts the real hal_flash_erase() and runs it against an emulated two-bank FAPI. --- hal/ti_hercules.c | 11 ++ tools/unit-tests/Makefile | 15 ++ tools/unit-tests/unit-ti-hercules-erase.c | 188 ++++++++++++++++++++++ 3 files changed, 214 insertions(+) create mode 100644 tools/unit-tests/unit-ti-hercules-erase.c diff --git a/hal/ti_hercules.c b/hal/ti_hercules.c index bb3fbca6c0..2386a6ef84 100644 --- a/hal/ti_hercules.c +++ b/hal/ti_hercules.c @@ -244,12 +244,23 @@ int RAMFUNCTION hal_flash_erase(uint32_t address, int len) uint32_t cur = 0; uint32_t end = address + len; + uint32_t bank_end = 0; Fapi_StatusType st = Fapi_getBankSectors(bank, &bank_sectors); if (st != Fapi_Status_Success) { return -1; } cur = bank_sectors.u32BankStartAddress; + /* Cross-bank erase is not supported: reject a range that extends + * past the end of the starting bank before erasing anything. */ + bank_end = cur; + for (i = 0; i < (int)bank_sectors.u32NumberOfSectors; i++) { + bank_end += (uint32_t)bank_sectors.au16SectorSizes[i] * 1024U; + } + if (end > bank_end) { + return -1; + } + hal_flash_unlock_helper(address); for(i=0; i < bank_sectors.u32NumberOfSectors; i++) { /* perfectly done */ diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index 9d3e6e03cf..ade84ef0dc 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -161,6 +161,7 @@ TESTS+=unit-sdhci-cmd-inhibit-uhs TESTS+=unit-sdhci-wait-busy TESTS+=unit-sdhci-acmd41-timeout TESTS+=unit-ti-hercules-write +TESTS+=unit-ti-hercules-erase TESTS+=unit-p1021-qe-firmware TESTS+=unit-t10xx-dts-memac TESTS+=unit-fs-probe @@ -1790,6 +1791,20 @@ ti_hercules_write_extract.h: ../../hal/ti_hercules.c unit-ti-hercules-write: unit-ti-hercules-write.c ti_hercules_write_extract.h gcc -o $@ unit-ti-hercules-write.c $(CFLAGS) $(LDFLAGS) +# unit-ti-hercules-erase runs the real hal_flash_erase() bank handling +# from hal/ti_hercules.c (a range crossing the end of the starting +# flash bank was partially erased and reported as success). +# Same extraction trick as the write test; the FAPI calls are +# emulated with a two-bank geometry. + +ti_hercules_erase_extract.h: ../../hal/ti_hercules.c + sed -n '/^static inline Fapi_FlashBankType RAMFUNCTION f021_lookup_bank/,/^}/p' $< > $@ + sed -n '/^static inline int RAMFUNCTION hal_flash_unlock_helper/,/^}/p' $< >> $@ + sed -n '/^int RAMFUNCTION hal_flash_erase(/,/^}/p' $< >> $@ + +unit-ti-hercules-erase: unit-ti-hercules-erase.c ti_hercules_erase_extract.h + gcc -o $@ unit-ti-hercules-erase.c $(CFLAGS) $(LDFLAGS) + # unit-p1021-qe-firmware drives the real qe_upload_firmware() from # hal/nxp_p1021.c (microcode offsets/counts were # never bounded to the 64 KiB buffer read from the update partition). diff --git a/tools/unit-tests/unit-ti-hercules-erase.c b/tools/unit-tests/unit-ti-hercules-erase.c new file mode 100644 index 0000000000..18c7cf51b2 --- /dev/null +++ b/tools/unit-tests/unit-ti-hercules-erase.c @@ -0,0 +1,188 @@ +/* unit-ti-hercules-erase.c + * + * Regression test: hal_flash_erase() in hal/ti_hercules.c selected the + * bank geometry from the starting address only and iterated that bank's + * sectors. A range extending past the end of the starting bank was + * partially erased and reported as success. Cross-bank requests must be + * rejected before anything is erased. + * + * The HAL needs the TI FAPI vendor headers and cannot be built on the + * host, so the Makefile extracts f021_lookup_bank(), + * hal_flash_unlock_helper() and hal_flash_erase() verbatim into + * ti_hercules_erase_extract.h; the FAPI calls are emulated with a + * two-bank geometry (bank0 [0x000000, 0x200000), bank1 + * [0x200000, 0x400000), two 0x100000 sectors per bank). + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#include +#include +#include +#include +#include + +/* The extracted functions are RAMFUNCTION; on the host that is nothing. */ +#define RAMFUNCTION + +/* FAPI stubs: two banks, two 0x100000 sectors each. */ +typedef int Fapi_FlashBankType; +#define Fapi_FlashBank0 0 +#define Fapi_FlashBank1 1 +typedef int Fapi_StatusType; +#define Fapi_Status_Success 0 +#define Fapi_Status_FsmReady 0 +#define FAPI_CHECK_FSM_READY_BUSY fapi_check_fsm() + +#define G_SECTORS_PER_BANK 2 + +typedef struct { + uint32_t u32BankStartAddress; + uint32_t u32NumberOfSectors; + uint16_t au16SectorSizes[G_SECTORS_PER_BANK]; +} Fapi_FlashBankSectorsType; + +static int fapi_check_fsm(void) +{ + return Fapi_Status_FsmReady; +} + +static Fapi_StatusType Fapi_getBankSectors(Fapi_FlashBankType bank, + Fapi_FlashBankSectorsType *sectors) +{ + sectors->u32BankStartAddress = (bank == Fapi_FlashBank1) ? 0x200000 : 0; + sectors->u32NumberOfSectors = G_SECTORS_PER_BANK; + sectors->au16SectorSizes[0] = 0x100000 / 1024; + sectors->au16SectorSizes[1] = 0x100000 / 1024; + return Fapi_Status_Success; +} + +static int Fapi_setActiveFlashBank(Fapi_FlashBankType bank) +{ + (void)bank; + return 0; +} + +static int Fapi_enableMainBankSectors(uint16_t en) +{ + (void)en; + return 0; +} + +/* Erase calls recorded by the stub. */ +#define MAX_ERASES 8 +static uint32_t g_erases[MAX_ERASES]; +static int g_erases_n; +static int g_erase_fail; + +static inline int f021_flash_erase(uint32_t address) +{ + if (g_erases_n < MAX_ERASES) { + g_erases[g_erases_n] = address; + g_erases_n++; + } + if (g_erase_fail) { + return -1; + } + return 0; +} + +void wolfBoot_printf(const char *format, ...) +{ + (void)format; +} + +/* The real functions from hal/ti_hercules.c (extracted by the Makefile). */ +#include "ti_hercules_erase_extract.h" + +static void setup(void) +{ + memset(g_erases, 0, sizeof(g_erases)); + g_erases_n = 0; + g_erase_fail = 0; +} + +static void teardown(void) +{ +} + +/* An erase aligned to one bank sector: success, exactly one erase. */ +START_TEST(test_erase_within_bank0) +{ + ck_assert_int_eq(hal_flash_erase(0x100000, 0x100000), 0); + ck_assert_int_eq(g_erases_n, 1); + ck_assert_uint_eq(g_erases[0], 0x100000); +} +END_TEST + +/* An erase in the second bank: the lookup picks bank1 and the request + * succeeds. */ +START_TEST(test_erase_within_bank1) +{ + ck_assert_int_eq(hal_flash_erase(0x200000, 0x100000), 0); + ck_assert_int_eq(g_erases_n, 1); + ck_assert_uint_eq(g_erases[0], 0x200000); +} +END_TEST + +/* The case: a range crossing the bank0/bank1 boundary must be rejected + * before anything is erased. */ +START_TEST(test_erase_crossing_bank_boundary_rejected) +{ + ck_assert_int_eq(hal_flash_erase(0x100000, 0x100001), -1); + ck_assert_int_eq(g_erases_n, 0); +} +END_TEST + +/* An erase of the whole bank0: success, both sectors erased. */ +START_TEST(test_erase_whole_bank0) +{ + ck_assert_int_eq(hal_flash_erase(0, 0x200000), 0); + ck_assert_int_eq(g_erases_n, 2); + ck_assert_uint_eq(g_erases[0], 0); + ck_assert_uint_eq(g_erases[1], 0x100000); +} +END_TEST + +Suite *ti_hercules_erase_suite(void) +{ + Suite *s = suite_create("ti-hercules-erase"); + TCase *tc = tcase_create("ti-hercules-erase"); + + tcase_add_checked_fixture(tc, setup, teardown); + tcase_add_test(tc, test_erase_within_bank0); + tcase_add_test(tc, test_erase_within_bank1); + tcase_add_test(tc, test_erase_crossing_bank_boundary_rejected); + tcase_add_test(tc, test_erase_whole_bank0); + + suite_add_tcase(s, tc); + return s; +} + +int main(void) +{ + int fails; + Suite *s = ti_hercules_erase_suite(); + SRunner *sr = srunner_create(s); + + srunner_run_all(sr, CK_NORMAL); + fails = srunner_ntests_failed(sr); + srunner_free(sr); + + return fails; +} From f219d093e12cd0e3ba972cfca81d55a1c5098911 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 10:53:48 +0200 Subject: [PATCH 29/50] F-12067: test_flash returns -1 on mismatch, 0 on success A mismatch at index zero returned -0 (success-looking), and a successful test returned the ext_flash_read() byte count. Return a fixed -1 on mismatch and 0 on success, and have the hal_init diagnostic caller log a failure. --- hal/nxp_ls1028a.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/hal/nxp_ls1028a.c b/hal/nxp_ls1028a.c index 4e29ca38e6..d7cde5a0cc 100644 --- a/hal/nxp_ls1028a.c +++ b/hal/nxp_ls1028a.c @@ -799,12 +799,12 @@ static int test_flash(void) wolfBoot_printf("check[%3d] %02x\n", i, pageData[i]); if (pageData[i] != (i & 0xff)) { wolfBoot_printf("Check Data @ %d failed\n", i); - return -i; + return -1; } } wolfBoot_printf("Flash Test Passed\n"); - return ret; + return 0; } #endif /* TEST_EXT_FLASH */ @@ -951,7 +951,9 @@ void hal_init(void) wolfBoot_printf("Flash init done\n"); #ifdef TEST_EXT_FLASH - test_flash(); + if (test_flash() != 0) { + wolfBoot_printf("External flash test FAILED\n"); + } #endif #ifdef TPM_TEST From c7bf895cc8b6f600b000f96cb127c52dad7fabfe Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 10:56:40 +0200 Subject: [PATCH 30/50] F-12886: reject non-positive length in HiFive1 flash erase For len <= 0 the inclusive end (address + len - 1) underflows and the sector loop runs through address wraparound. Treat a non-positive length as a no-op before computing the end. --- hal/hifive1.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/hal/hifive1.c b/hal/hifive1.c index 118b8a955c..508873f251 100644 --- a/hal/hifive1.c +++ b/hal/hifive1.c @@ -573,8 +573,14 @@ int RAMFUNCTION hal_flash_erase(uint32_t address, int len) { uint32_t end; uint32_t p; - if (address >= FLASH_BASE) + + /* A non-positive length would underflow the inclusive end below. */ + if (len <= 0) { + return 0; + } + if (address >= FLASH_BASE) { address -= FLASH_BASE; + } end = address + len - 1; FESPI_REG_TXMARK = 1; From 3d988c74cb67ac795abe29ce28421e352104ba11 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 10:58:42 +0200 Subject: [PATCH 31/50] F-12919: log FIT compression from a bounded NUL-terminated copy The compression property is only known to be NUL-terminated for the recognized values; the unsupported and no-destination diagnostics formatted the raw property with %s, letting a malformed value read past the property in uart_vprintf's strlen. Copy at most 23 bytes into a local buffer and log that. --- src/fdt.c | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/fdt.c b/src/fdt.c index 1bfb96caaa..60b28f57af 100644 --- a/src/fdt.c +++ b/src/fdt.c @@ -1692,6 +1692,8 @@ static void* fit_load_image_inner(fdt_ctx* ctx, const char* image, int* lenp, int off, len = 0; const char *comp; int complen = 0; + char compstr[24]; + int n; #ifdef WOLFBOOT_GZIP BENCHMARK_DECLARE(); #endif @@ -1742,6 +1744,16 @@ static void* fit_load_image_inner(fdt_ctx* ctx, const char* image, int* lenp, else { is_unknown_comp = 1; } + + /* Bounded NUL-terminated copy for the diagnostics: the + * raw property is only known to be terminated for the + * recognized values. */ + n = complen; + if (n > 23) { + n = 23; + } + memcpy(compstr, comp, (size_t)n); + compstr[n] = '\0'; } if (load != NULL && data != load) { if (is_gzip) { @@ -1804,7 +1816,7 @@ static void* fit_load_image_inner(fdt_ctx* ctx, const char* image, int* lenp, /* Unknown compression scheme; fail closed rather * than silently memcpy compressed bytes as raw. */ wolfBoot_printf("FIT: subimage '%s' has unsupported " - "compression=\"%s\"\n", image, comp); + "compression=\"%s\"\n", image, compstr); return NULL; } else { @@ -1847,7 +1859,7 @@ static void* fit_load_image_inner(fdt_ctx* ctx, const char* image, int* lenp, "compression=\"%s\" but has no distinct load " "destination (load=%p, data=%p); refusing to pass " "compressed bytes through as raw\n", - image, comp, load, data); + image, compstr, load, data); return NULL; } } From ef122f63fc6105b199ebf63d30aa5d8d854126f7 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 11:01:51 +0200 Subject: [PATCH 32/50] F-13608: check flash read/hash results in ELF image verification wolfBoot_check_flash_image_elf() discarded the results of every read_flash_fwimage() and update_hash_flash_fwimg() call, so a rejected range left ph_buf/ph_next_buf stale or uninitialized and the parser consumed indeterminate values. Check each result and return -1 on failure, matching the pattern wolfBoot_load_flash_image_elf already uses. --- src/image.c | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/src/image.c b/src/image.c index 42d4505780..3aff62874c 100644 --- a/src/image.c +++ b/src/image.c @@ -2241,7 +2241,10 @@ int wolfBoot_check_flash_image_elf(uint8_t part, unsigned long* entry_out) /* Get the elf header from the image into a local buffer. We may overread * the buffer depending on architecture */ memset(elfHdrBuf, 0, sizeof(elfHdrBuf)); - read_flash_fwimage(&boot, 0, elfHdrBuf, sizeof(elfHeaderMaxBuf)); + if (read_flash_fwimage(&boot, 0, elfHdrBuf, + sizeof(elfHeaderMaxBuf)) != 0) { + return -1; + } elf_h = elfHdrBuf; if (elf_open(elf_h, &is_elf32) < 0) { @@ -2275,14 +2278,18 @@ int wolfBoot_check_flash_image_elf(uint8_t part, unsigned long* entry_out) /* Hash the elf header and program header in the image, assuming the PHT * immediately follows the ELF header */ - update_hash_flash_fwimg(&ctx, &boot, 0, elf_hdr_sz); + if (update_hash_flash_fwimg(&ctx, &boot, 0, elf_hdr_sz) != 0) { + return -1; + } current_ph_offset = entry_off; /* Calculate padding between ELF+PHT header and first segment */ if (entry_count > 0) { uint64_t first_offset; - read_flash_fwimage(&boot, current_ph_offset, ph_buf, ph_size); + if (read_flash_fwimage(&boot, current_ph_offset, ph_buf, ph_size) != 0) { + return -1; + } if (is_elf32) { first_offset = ((elf32_program_header*)ph_buf)->offset; } @@ -2295,7 +2302,10 @@ int wolfBoot_check_flash_image_elf(uint8_t part, unsigned long* entry_out) wolfBoot_printf( "ELF: [CHECK] Adding %d bytes padding before first segment\n", (int32_t)len); - update_hash_flash_fwimg(&ctx, &boot, elf_hdr_sz, len); /* Hash actual file content */ + /* Hash actual file content */ + if (update_hash_flash_fwimg(&ctx, &boot, elf_hdr_sz, len) != 0) { + return -1; + } } } @@ -2308,7 +2318,9 @@ int wolfBoot_check_flash_image_elf(uint8_t part, unsigned long* entry_out) uint64_t next_offset = 0; /* Initialize */ /* read the current program header into a local buffer */ - read_flash_fwimage(&boot, current_ph_offset, ph_buf, ph_size); + if (read_flash_fwimage(&boot, current_ph_offset, ph_buf, ph_size) != 0) { + return -1; + } /* Extract common fields based on ELF type */ if (is_elf32) { @@ -2375,8 +2387,10 @@ int wolfBoot_check_flash_image_elf(uint8_t part, unsigned long* entry_out) /* Add padding until next program header, if any. */ if (i < entry_count - 1) { - read_flash_fwimage(&boot, current_ph_offset + ph_size, ph_next_buf, - ph_size); + if (read_flash_fwimage(&boot, current_ph_offset + ph_size, + ph_next_buf, ph_size) != 0) { + return -1; + } if (is_elf32) { next_offset = ((elf32_program_header*)ph_next_buf)->offset; } @@ -2390,7 +2404,11 @@ int wolfBoot_check_flash_image_elf(uint8_t part, unsigned long* entry_out) "0x%08lx to 0x%08lx)\n", padding, (unsigned long)(offset + filesz), (unsigned long)next_offset); - update_hash_flash_fwimg(&ctx, &boot, offset + filesz, padding); /* Hash actual file content */ + /* Hash actual file content */ + if (update_hash_flash_fwimg(&ctx, &boot, offset + filesz, + padding) != 0) { + return -1; + } } } @@ -2422,7 +2440,9 @@ int wolfBoot_check_flash_image_elf(uint8_t part, unsigned long* entry_out) wolfBoot_printf("ELF: [CHECK] Hashing %u bytes of trailing data from " "offset 0x%llX\n", len, (unsigned long long)final_offset); - update_hash_flash_fwimg(&ctx, &boot, final_offset, len); + if (update_hash_flash_fwimg(&ctx, &boot, final_offset, len) != 0) { + return -1; + } } From 13448ab22637218e90277441531263d3ca4c9f68 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 11:14:01 +0200 Subject: [PATCH 33/50] F-13609: consume over-long BOOT_ORDER tokens whole in env_next_name A token longer than the name buffer left its remainder in the stream, which the next call read as a second, spurious slot name. env_next_name now consumes the whole token and returns -1; the select_slot gate treats <= 0 as malformed. Adds unit tests driving the static directly (70-char token with and without a trailing valid name). --- src/ubootenv.c | 10 +++++++- tools/unit-tests/unit-ubootenv.c | 41 ++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/ubootenv.c b/src/ubootenv.c index 4e61bf5578..240e67ae05 100644 --- a/src/ubootenv.c +++ b/src/ubootenv.c @@ -310,6 +310,14 @@ static int env_next_name(const char **po, char *name, size_t name_max) while (*o != '\0' && *o != ' ' && n < (int)name_max - 1) name[n++] = *o++; name[n] = '\0'; + if (*o != '\0' && *o != ' ') { + /* Token longer than the buffer: consume the remainder so it + * does not resurface as a second, spurious slot name. */ + while (*o != '\0' && *o != ' ') + o++; + *po = o; + return -1; + } *po = o; return n; } @@ -357,7 +365,7 @@ int uboot_env_select_slot(uint8_t *env, size_t env_len, struct uboot_slot *out) /* Malformed (empty / whitespace-only) BOOT_ORDER yields no tokens: report * failure per the header contract rather than "success, no slot". */ o = order; - if (env_next_name(&o, name, sizeof(name)) == 0) + if (env_next_name(&o, name, sizeof(name)) <= 0) return -1; /* Pass 1: first slot with tries left -> select + decrement. */ diff --git a/tools/unit-tests/unit-ubootenv.c b/tools/unit-tests/unit-ubootenv.c index 0953a4e245..263441cc76 100644 --- a/tools/unit-tests/unit-ubootenv.c +++ b/tools/unit-tests/unit-ubootenv.c @@ -373,6 +373,45 @@ START_TEST(test_select_malformed_counter_skips_slot) } END_TEST +/* env_next_name() is static; the test includes the .c directly, so drive it + * with a token longer than the name buffer: the whole token must be + * consumed and reported as truncated (-1), not left to resurface as a + * second, spurious slot name. */ +START_TEST(test_next_name_overlong_consumed_whole) +{ + const char *s = "xxxxxxxxxx" "xxxxxxxxxx" "xxxxxxxxxx" + "xxxxxxxxxx" "xxxxxxxxxx" "xxxxxxxxxx" + "xxxxxxxxxx" " A"; /* 70 x's, then A */ + const char *o = s; + char name[UBOOT_ENV_VAL_MAX]; + int n; + + n = env_next_name(&o, name, sizeof(name)); + ck_assert_int_eq(n, -1); + n = env_next_name(&o, name, sizeof(name)); + ck_assert_int_eq(n, 1); + ck_assert_str_eq(name, "A"); +} +END_TEST + +/* A stream of a single over-long token: truncated (-1), then end of stream. + * No spurious second token. */ +START_TEST(test_next_name_only_overlong) +{ + const char *s = "xxxxxxxxxx" "xxxxxxxxxx" "xxxxxxxxxx" + "xxxxxxxxxx" "xxxxxxxxxx" "xxxxxxxxxx" + "xxxxxxxxxx"; /* 70 x's */ + const char *o = s; + char name[UBOOT_ENV_VAL_MAX]; + int n; + + n = env_next_name(&o, name, sizeof(name)); + ck_assert_int_eq(n, -1); + n = env_next_name(&o, name, sizeof(name)); + ck_assert_int_eq(n, 0); +} +END_TEST + Suite *ubootenv_suite(void) { Suite *s = suite_create("ubootenv"); @@ -399,6 +438,8 @@ Suite *ubootenv_suite(void) tcase_add_test(tc, test_atol_rejects_overlong); tcase_add_test(tc, test_ltoa_respects_bound); tcase_add_test(tc, test_select_malformed_counter_skips_slot); + tcase_add_test(tc, test_next_name_overlong_consumed_whole); + tcase_add_test(tc, test_next_name_only_overlong); suite_add_tcase(s, tc); return s; From 455e5358e6f0070c3e0121bce2972b4d1db8c758 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 11:26:10 +0200 Subject: [PATCH 34/50] F-13611: drop the shadowed dec_block in ext_flash_decrypt_read The inner dec_block declaration shadowed the function-scope buffer; the trailer now decrypts into the outer buffer. --- src/libwolfboot.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libwolfboot.c b/src/libwolfboot.c index 498171d7c8..2a052b12f1 100644 --- a/src/libwolfboot.c +++ b/src/libwolfboot.c @@ -2942,7 +2942,6 @@ int RAMFUNCTION ext_flash_decrypt_read(uintptr_t address, uint8_t *data, int len unaligned_trailer_size = read_remaining; if (unaligned_trailer_size > 0) { - uint8_t dec_block[ENCRYPT_BLOCK_SIZE] XALIGNED_STACK(4); if (ext_flash_read(address, block, ENCRYPT_BLOCK_SIZE) != ENCRYPT_BLOCK_SIZE) return -1; From 07b573e09c59212fc89d8a3664e4d62b1cd7851e Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 11:26:36 +0200 Subject: [PATCH 35/50] unit-tests: gitignore the generated extract headers and test binaries Per-file entries, matching the existing convention; covers the artifacts added by recent unit-test campaigns that were never registered. --- .gitignore | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/.gitignore b/.gitignore index 794e9830f8..b9c3c54a4e 100644 --- a/.gitignore +++ b/.gitignore @@ -563,3 +563,71 @@ fs_b.img tools/unit-tests/aarch64_arch_host.h tools/unit-tests/imx8qm_usdhc_extract.h tools/unit-tests/imx8qm_flexspi_extract.h +# unit-test binaries added since the last gitignore pass +tools/unit-tests/unit-ct-compare +tools/unit-tests/unit-custom-trailer-nopart +tools/unit-tests/unit-ecc-raw-der +tools/unit-tests/unit-elf-mmu-fail +tools/unit-tests/unit-elf-phentsize +tools/unit-tests/unit-fdt-memrsv-wrap +tools/unit-tests/unit-flash-erase-kinetis +tools/unit-tests/unit-flash-m2354 +tools/unit-tests/unit-fwtpm-cmd-toctou +tools/unit-tests/unit-fwtpm-rsp-overrun +tools/unit-tests/unit-hifive1-flash-write +tools/unit-tests/unit-hifive1-hfrosccfg +tools/unit-tests/unit-keygen-keystore +tools/unit-tests/unit-kontron-tgl-spi +tools/unit-tests/unit-nrf5340-flash-protect +tools/unit-tests/unit-nrf5340-uart-crlf +tools/unit-tests/unit-nrf54l-uart-write +tools/unit-tests/unit-nsc-update +tools/unit-tests/unit-p1021-erase-advance +tools/unit-tests/unit-p1021-read-badblock +tools/unit-tests/unit-pci-4gib +tools/unit-tests/unit-pkcs11-pin-zeroize +tools/unit-tests/unit-pkcs11_store-stalecache +tools/unit-tests/unit-rp2350-flash-write +tools/unit-tests/unit-samr21-erase-advance +tools/unit-tests/unit-sdhci-acmd41-timeout +tools/unit-tests/unit-secure-handoff +tools/unit-tests/unit-stm32c0-write +tools/unit-tests/unit-stm32g4-write +tools/unit-tests/unit-stm32h7-flash-write +tools/unit-tests/unit-stm32l4-write +tools/unit-tests/unit-stm32u3-flash-write +tools/unit-tests/unit-stm32wb-write +tools/unit-tests/unit-t10xx-flash-status +tools/unit-tests/unit-ti-hercules-erase +tools/unit-tests/unit-update-disk-fsp +tools/unit-tests/unit-update-ram-custom-trailer +tools/unit-tests/unit-update-ram-nofixed-noramboot +tools/unit-tests/unit-update-trigger-scrub +tools/unit-tests/unit-x86-uart +# sources generated by the unit-test extraction rules +tools/unit-tests/ct_compare_extract.h +tools/unit-tests/fdt_memrsv_extract.h +tools/unit-tests/hifive1_flash_write_extract.h +tools/unit-tests/hifive1_hfrosccfg_extract.h +tools/unit-tests/keystore_api_extract.h +tools/unit-tests/keystore_emitted.c +tools/unit-tests/keystore_gen +tools/unit-tests/kinetis_erase_extract.h +tools/unit-tests/kontron_spi_extract.h +tools/unit-tests/kontron_spi_fn_extract.h +tools/unit-tests/nrf5340_protect_fn_extract.h +tools/unit-tests/nrf54l_uart_write_extract.h +tools/unit-tests/p1021_erase_extract.h +tools/unit-tests/p1021_erase_fn_extract.h +tools/unit-tests/p1021_read_extract.h +tools/unit-tests/p1021_read_fn_extract.h +tools/unit-tests/rp2350_flash_write_extract.h +tools/unit-tests/stm32c0_write_extract.h +tools/unit-tests/stm32g4_write_extract.h +tools/unit-tests/stm32h7_flash_write_extract.h +tools/unit-tests/stm32l4_write_extract.h +tools/unit-tests/stm32u3_flash_write_extract.h +tools/unit-tests/stm32wb_write_extract.h +tools/unit-tests/t10xx_flash_status_extract.h +tools/unit-tests/ti_hercules_erase_extract.h +tools/unit-tests/versal_host_write.c From 641c9cf4a0b03cf4e59855d98da8da940a658f3d Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 12:01:43 +0200 Subject: [PATCH 36/50] F-12871: compare against the absolute bank 2 base The dispatcher split cross-bank requests at FLASH_BANK2_BASE_REL (0x00100000), but callers pass absolute flash addresses in FLASHMEM_ADDRESS_SPACE, so the split never fired and the whole request was programmed on the bank of the start address. Compare against FLASH_BANK2_BASE (0x08100000); the test now drives absolute addresses so it exercises the real call convention. --- hal/stm32h7.c | 7 +++--- tools/unit-tests/unit-stm32h7-flash-write.c | 28 +++++++++++---------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/hal/stm32h7.c b/hal/stm32h7.c index 0241b7fcd0..83e8cd6c9a 100644 --- a/hal/stm32h7.c +++ b/hal/stm32h7.c @@ -96,13 +96,14 @@ static int RAMFUNCTION hal_flash_write_part(uint32_t address, /* * hal_flash_write() dispatches across the bank boundary: a request that * starts in bank 1 and extends into bank 2 is split at - * FLASH_BANK2_BASE_REL, because the programming path below selects one + * FLASH_BANK2_BASE, because the programming path below selects one * bank (FLASH_CR1/CR2) for the whole request and would write the tail - * into the wrong bank (F-12871). + * into the wrong bank (F-12871). The comparison is against the absolute + * bank 2 base: callers pass flash addresses in FLASHMEM_ADDRESS_SPACE. */ int RAMFUNCTION hal_flash_write(uint32_t address, const uint8_t *data, int len) { - uint32_t bank2 = FLASH_BANK2_BASE_REL; + uint32_t bank2 = FLASH_BANK2_BASE; if ((address < bank2) && ((address + (uint32_t)len) > bank2)) { uint32_t first = bank2 - address; diff --git a/tools/unit-tests/unit-stm32h7-flash-write.c b/tools/unit-tests/unit-stm32h7-flash-write.c index cc19a96831..20917340a3 100644 --- a/tools/unit-tests/unit-stm32h7-flash-write.c +++ b/tools/unit-tests/unit-stm32h7-flash-write.c @@ -7,7 +7,8 @@ * FLASH_BANK2_BASE_REL. * * The dispatcher is extracted by the Makefile; hal_flash_write_part() - * is a recording mock. + * is a recording mock. Addresses are absolute, as passed by callers + * (FLASHMEM_ADDRESS_SPACE based); the split point is FLASH_BANK2_BASE. * * Copyright (C) 2026 wolfSSL Inc. * @@ -35,7 +36,8 @@ #define RAMFUNCTION #define FLASH_BANK_1 0 #define FLASH_BANK_2 1 -#define FLASH_BANK2_BASE_REL 0x100000u +#define FLASHMEM_ADDRESS_SPACE 0x08000000u +#define FLASH_BANK2_BASE 0x08100000u /* The real dispatcher from hal/stm32h7.c (extracted by the Makefile); * it calls hal_flash_write_part(), mocked below (prototype first). */ @@ -80,16 +82,17 @@ static void teardown(void) /* A request crossing the boundary must be split: first pass up to the * boundary on bank 1, remainder starting at the boundary on bank 2. */ -START_TEST(test_write_crosses_boundary){ +START_TEST(test_write_crosses_boundary) +{ uint8_t data[64]; memset(data, 0xAA, sizeof(data)); - ck_assert_int_eq(hal_flash_write(FLASH_BANK2_BASE_REL - 16, data, 32), 0); + ck_assert_int_eq(hal_flash_write(FLASH_BANK2_BASE - 16, data, 32), 0); ck_assert_int_eq(g_calls, 2); - ck_assert_uint_eq(g_addr[0], FLASH_BANK2_BASE_REL - 16); + ck_assert_uint_eq(g_addr[0], FLASH_BANK2_BASE - 16); ck_assert_int_eq(g_len[0], 16); - ck_assert_uint_eq(g_addr[1], FLASH_BANK2_BASE_REL); + ck_assert_uint_eq(g_addr[1], FLASH_BANK2_BASE); ck_assert_int_eq(g_len[1], 16); } END_TEST @@ -101,9 +104,10 @@ START_TEST(test_write_bank1_only) memset(data, 0xAA, sizeof(data)); - ck_assert_int_eq(hal_flash_write(0x400, data, 16), 0); + ck_assert_int_eq(hal_flash_write(FLASHMEM_ADDRESS_SPACE + 0x400, data, 16), + 0); ck_assert_int_eq(g_calls, 1); - ck_assert_uint_eq(g_addr[0], 0x400); + ck_assert_uint_eq(g_addr[0], FLASHMEM_ADDRESS_SPACE + 0x400); ck_assert_int_eq(g_len[0], 16); } END_TEST @@ -115,10 +119,9 @@ START_TEST(test_write_bank2_only) memset(data, 0xAA, sizeof(data)); - ck_assert_int_eq(hal_flash_write(FLASH_BANK2_BASE_REL + 0x400, data, 16), - 0); + ck_assert_int_eq(hal_flash_write(FLASH_BANK2_BASE + 0x400, data, 16), 0); ck_assert_int_eq(g_calls, 1); - ck_assert_uint_eq(g_addr[0], FLASH_BANK2_BASE_REL + 0x400); + ck_assert_uint_eq(g_addr[0], FLASH_BANK2_BASE + 0x400); ck_assert_int_eq(g_len[0], 16); } END_TEST @@ -131,8 +134,7 @@ START_TEST(test_write_first_part_fails) memset(data, 0xAA, sizeof(data)); g_first_ret = -1; - ck_assert_int_eq(hal_flash_write(FLASH_BANK2_BASE_REL - 16, data, 32), - -1); + ck_assert_int_eq(hal_flash_write(FLASH_BANK2_BASE - 16, data, 32), -1); ck_assert_int_eq(g_calls, 1); } END_TEST From f528cabfd01ee29515aa870f704870e1ab766d24 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 12:01:43 +0200 Subject: [PATCH 37/50] F-14131: pass the byte length to FLASH_EraseSector The MCXA ROM API (fsl_romapi.h) takes lengthInBytes, not a sector count, so the original pass-through was already correct and the sector-count conversion erased almost nothing. It also called FLASH_GetFlashConfig, which does not exist in the MCXA ROM API and only compiled against the unit-test stub. Restore the byte length, keep the len guard, and drop the invented call from the stub. --- hal/mcxa.c | 16 +++------ tools/unit-tests/mcxa_fsl_stub/fsl_common.h | 3 +- tools/unit-tests/mcxa_fsl_stub/fsl_romapi.h | 9 ++--- tools/unit-tests/unit-flash-write-mcxa.c | 38 +++++++-------------- 4 files changed, 24 insertions(+), 42 deletions(-) diff --git a/hal/mcxa.c b/hal/mcxa.c index de7a59c7b4..ec38026609 100644 --- a/hal/mcxa.c +++ b/hal/mcxa.c @@ -120,19 +120,13 @@ void RAMFUNCTION hal_flash_lock(void) int RAMFUNCTION hal_flash_erase(uint32_t address, int len) { - flash_config_t config; - uint32_t sector_count; - if (len <= 0) return -1; - /* FLASH_EraseSector takes a sector count, not a byte length; round - * the byte range up to whole sectors. */ - if (FLASH_GetFlashConfig(&pflash, &config) != kStatus_Success) - return -1; - sector_count = (uint32_t)len / config.sectorSize; - if (((uint32_t)len % config.sectorSize) != 0U) - sector_count++; - if (FLASH_EraseSector(&pflash, address, sector_count, kFLASH_ApiEraseKey) + while ((address % 4) != 0) + address--; + /* FLASH_EraseSector takes the length in bytes (4-byte aligned), + * not a sector count (MCXA ROM API, fsl_romapi.h). */ + if (FLASH_EraseSector(&pflash, address, (uint32_t)len, kFLASH_ApiEraseKey) != kStatus_Success) return -1; return 0; diff --git a/tools/unit-tests/mcxa_fsl_stub/fsl_common.h b/tools/unit-tests/mcxa_fsl_stub/fsl_common.h index ec5f4a8f38..5782926268 100644 --- a/tools/unit-tests/mcxa_fsl_stub/fsl_common.h +++ b/tools/unit-tests/mcxa_fsl_stub/fsl_common.h @@ -10,8 +10,9 @@ typedef int status_t; #define kStatus_Success 0 +/* Opaque driver state in the real SDK; the stub needs no fields. */ typedef struct { - uint32_t sectorSize; + uint32_t unused; } flash_config_t; #endif /* FSL_COMMON_STUB_H */ diff --git a/tools/unit-tests/mcxa_fsl_stub/fsl_romapi.h b/tools/unit-tests/mcxa_fsl_stub/fsl_romapi.h index f569118058..e5c107a103 100644 --- a/tools/unit-tests/mcxa_fsl_stub/fsl_romapi.h +++ b/tools/unit-tests/mcxa_fsl_stub/fsl_romapi.h @@ -1,14 +1,15 @@ /* Minimal stand-in for the NXP MCUXpresso SDK ROM flash API declarations * used by hal/mcxa.c's hal_flash_write()/hal_flash_erase(). The unit test - * provides the definitions of these functions. */ + * provides the definitions of these functions. Parameter names match the + * real MCXA ROM API (fsl_romapi.h): FLASH_EraseSector takes a length in + * bytes, not a sector count. */ #ifndef FSL_ROMAPI_STUB_H #define FSL_ROMAPI_STUB_H status_t FLASH_ProgramPhrase(flash_config_t *config, uint32_t start, - uint8_t *src, uint32_t len); + uint8_t *src, uint32_t length_in_bytes); status_t FLASH_EraseSector(flash_config_t *config, uint32_t start, - uint32_t sector_count, uint32_t key); -status_t FLASH_GetFlashConfig(flash_config_t *state, flash_config_t *config); + uint32_t length_in_bytes, uint32_t key); #define kFLASH_ApiEraseKey 0x6b65796b diff --git a/tools/unit-tests/unit-flash-write-mcxa.c b/tools/unit-tests/unit-flash-write-mcxa.c index a48c1da0c6..a97d2559ca 100644 --- a/tools/unit-tests/unit-flash-write-mcxa.c +++ b/tools/unit-tests/unit-flash-write-mcxa.c @@ -52,7 +52,7 @@ /* Records of the (mock) ROM erase calls issued by hal_flash_erase(). */ static int erase_calls; static uint32_t last_erase_start; -static uint32_t last_erase_count; +static uint32_t last_erase_len; status_t FLASH_ProgramPhrase(flash_config_t *config, uint32_t start, uint8_t *src, uint32_t len) @@ -63,20 +63,12 @@ status_t FLASH_ProgramPhrase(flash_config_t *config, uint32_t start, } status_t FLASH_EraseSector(flash_config_t *config, uint32_t start, - uint32_t sector_count, uint32_t key) + uint32_t length_in_bytes, uint32_t key) { (void)config; (void)key; erase_calls++; last_erase_start = start; - last_erase_count = sector_count; - return kStatus_Success; -} - -status_t FLASH_GetFlashConfig(flash_config_t *state, flash_config_t *config) -{ - (void)state; - /* i.MX RT5xx internal flash sector size */ - config->sectorSize = 4096U; + last_erase_len = length_in_bytes; return kStatus_Success; } @@ -98,7 +90,7 @@ static void setup(void) memset(mock_flash, 0xFF, MOCK_FLASH_SIZE); erase_calls = 0; last_erase_start = 0; - last_erase_count = 0; + last_erase_len = 0; } static void teardown(void) @@ -180,32 +172,26 @@ START_TEST(test_aligned_write_bulk_then_tail) } END_TEST -/* hal_flash_erase() must hand FLASH_EraseSector a sector count, not a byte - * length (the ROM API erases one sector per count unit). A 4 KiB request is - * one sector; a partial extra byte rounds up to the next sector. */ -START_TEST(test_erase_byte_len_converted_to_sector_count){ +/* hal_flash_erase() must hand FLASH_EraseSector the byte length + * unconverted (the MCXA ROM API takes lengthInBytes, not a sector + * count). */ +START_TEST(test_erase_passes_byte_length){ uint32_t base = (uint32_t)(uintptr_t)mock_flash; ck_assert_int_eq(hal_flash_erase(base, 4096), 0); ck_assert_int_eq(erase_calls, 1); ck_assert_uint_eq(last_erase_start, base); - ck_assert_uint_eq(last_erase_count, 1); - - erase_calls = 0; - ck_assert_int_eq(hal_flash_erase(base, 4097), 0); - ck_assert_int_eq(erase_calls, 1); - ck_assert_uint_eq(last_erase_count, 2); + ck_assert_uint_eq(last_erase_len, 4096); erase_calls = 0; ck_assert_int_eq(hal_flash_erase(base, 8192), 0); ck_assert_int_eq(erase_calls, 1); - ck_assert_uint_eq(last_erase_count, 2); + ck_assert_uint_eq(last_erase_len, 8192); } END_TEST /* A zero-length erase request must be rejected without touching the ROM - * API (before the fix the old alignment loop ran and the byte length was - * passed straight through as the sector count). */ + * API. */ START_TEST(test_erase_zero_len){ uint32_t base = (uint32_t)(uintptr_t)mock_flash; @@ -231,7 +217,7 @@ Suite *flash_write_suite(void) tcase_add_test(tc, test_unaligned_write_spanning_two_words); tcase_add_test(tc, test_unaligned_write_single_word); tcase_add_test(tc, test_aligned_write_bulk_then_tail); - tcase_add_test(tc, test_erase_byte_len_converted_to_sector_count); + tcase_add_test(tc, test_erase_passes_byte_length); tcase_add_test(tc, test_erase_zero_len); tcase_add_test(tc, test_erase_negative_len); From c2364149d84b6f8f8af202688e19143520847bc8 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 12:01:52 +0200 Subject: [PATCH 38/50] F-13608: check update_hash_flash_addr in ELF hashing A failed hash update was silently ignored, so a corrupt read of a loadable segment could still pass image verification. --- src/image.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/image.c b/src/image.c index 3aff62874c..aaec19c612 100644 --- a/src/image.c +++ b/src/image.c @@ -2377,8 +2377,10 @@ int wolfBoot_check_flash_image_elf(uint8_t part, unsigned long* entry_out) "offset = 0x%08lx, size = %lu\n", (unsigned long)paddr, (unsigned long)load_addr, (unsigned long)offset, (unsigned long)filesz); - update_hash_flash_addr(&ctx, load_addr, (uint32_t)filesz, - PART_IS_EXT(&boot)); + if (update_hash_flash_addr(&ctx, load_addr, (uint32_t)filesz, + PART_IS_EXT(&boot)) != 0) { + return -1; + } } else { wolfBoot_printf("ELF: [CHECK] ERROR: non-loadable segment\n"); From 75707384fa3f7522ca1e0133c116573df6a8c3b2 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 12:01:52 +0200 Subject: [PATCH 39/50] F-6871: bound ext_flash_erase to the device size Erase was the only ext_flash op without the layout check, so an out-of-range request reached the SDK. Also replace the lone _Static_assert with the tree's typedef char idiom. --- hal/rtl8735b.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hal/rtl8735b.c b/hal/rtl8735b.c index c7f0428aa9..8718ae6f6d 100644 --- a/hal/rtl8735b.c +++ b/hal/rtl8735b.c @@ -109,8 +109,8 @@ static uint8_t hal_bl_scratch[2048] * The SDK struct sizes are only known in the SDK backend -- when SPIC * adaptor reuse is enabled, re-derive the slots from sizeof() of the real * structs and keep this budget check. */ -_Static_assert(sizeof(hal_bl_scratch) >= 6 * 256 + 512, - "hal_bl_scratch must cover the seven bootloader slots"); +typedef char hal_bl_scratch_budget_check[ + (sizeof(hal_bl_scratch) >= 6 * 256 + 512) ? 1 : -1]; /* Exactly the 10 bytes "AmebaPro2\xff" (the trailing 0xff matters; a NUL pad * fails as "Invalid FW Image Signature"). */ @@ -512,7 +512,7 @@ int ext_flash_erase(uintptr_t address, int len) uint32_t sector_addr; uint32_t end_addr; - if (len < 0 || (uint32_t)len > UINT32_MAX - (uint32_t)address) { + if (!ext_flash_in_layout(address, len)) { return -1; } if (len == 0) { From 839a1d2d9e74e0f24752e940c4ee4d5b3bfdd1b0 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 12:01:52 +0200 Subject: [PATCH 40/50] F-13609: return the truncated length for over-long tokens Returning -1 for a truncated token contradicted the documented contract (token length, 0 at end of list) and skipped the caller's existing skip path for unmatchable names. Return the truncated length: env_leftkey fails to match it and the slot is skipped. --- src/ubootenv.c | 11 ++++++----- tools/unit-tests/unit-ubootenv.c | 13 +++++++------ 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/ubootenv.c b/src/ubootenv.c index 240e67ae05..064185a585 100644 --- a/src/ubootenv.c +++ b/src/ubootenv.c @@ -299,7 +299,9 @@ int uboot_env_set(uint8_t *env, size_t env_len, const char *key, } /* Copy the next space-separated token of BOOT_ORDER into name; advance *po. - * Returns the token length (0 when the list is exhausted). */ + * Returns the token length (0 when the list is exhausted); a token longer + * than name_max is truncated to name_max - 1 and the remainder consumed, + * so it does not resurface as a second, spurious slot name. */ static int env_next_name(const char **po, char *name, size_t name_max) { const char *o = *po; @@ -311,12 +313,11 @@ static int env_next_name(const char **po, char *name, size_t name_max) name[n++] = *o++; name[n] = '\0'; if (*o != '\0' && *o != ' ') { - /* Token longer than the buffer: consume the remainder so it - * does not resurface as a second, spurious slot name. */ + /* Token longer than the buffer: consume the remainder. The + * truncated token is returned as-is; callers that cannot match + * it (env_leftkey) skip the slot, which covers this case. */ while (*o != '\0' && *o != ' ') o++; - *po = o; - return -1; } *po = o; return n; diff --git a/tools/unit-tests/unit-ubootenv.c b/tools/unit-tests/unit-ubootenv.c index 263441cc76..c8a4a411e3 100644 --- a/tools/unit-tests/unit-ubootenv.c +++ b/tools/unit-tests/unit-ubootenv.c @@ -374,9 +374,9 @@ START_TEST(test_select_malformed_counter_skips_slot) END_TEST /* env_next_name() is static; the test includes the .c directly, so drive it - * with a token longer than the name buffer: the whole token must be - * consumed and reported as truncated (-1), not left to resurface as a - * second, spurious slot name. */ + * with a token longer than the name buffer: the token is truncated to + * name_max - 1, the whole remainder is consumed, and the next token is + * still parsed - nothing resurfaces as a spurious slot name. */ START_TEST(test_next_name_overlong_consumed_whole) { const char *s = "xxxxxxxxxx" "xxxxxxxxxx" "xxxxxxxxxx" @@ -387,14 +387,15 @@ START_TEST(test_next_name_overlong_consumed_whole) int n; n = env_next_name(&o, name, sizeof(name)); - ck_assert_int_eq(n, -1); + ck_assert_int_eq(n, UBOOT_ENV_VAL_MAX - 1); + ck_assert_int_eq(name[UBOOT_ENV_VAL_MAX - 2], 'x'); n = env_next_name(&o, name, sizeof(name)); ck_assert_int_eq(n, 1); ck_assert_str_eq(name, "A"); } END_TEST -/* A stream of a single over-long token: truncated (-1), then end of stream. +/* A stream of a single over-long token: truncated, then end of stream. * No spurious second token. */ START_TEST(test_next_name_only_overlong) { @@ -406,7 +407,7 @@ START_TEST(test_next_name_only_overlong) int n; n = env_next_name(&o, name, sizeof(name)); - ck_assert_int_eq(n, -1); + ck_assert_int_eq(n, UBOOT_ENV_VAL_MAX - 1); n = env_next_name(&o, name, sizeof(name)); ck_assert_int_eq(n, 0); } From d07d9818d27845ec36ec625c7843af1704b9a004 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 12:03:20 +0200 Subject: [PATCH 41/50] F-12067: check erase/write results in test_flash Only the final read result reached the return value; a failed erase or write was reported as success. --- hal/nxp_ls1028a.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hal/nxp_ls1028a.c b/hal/nxp_ls1028a.c index d7cde5a0cc..b329cd7620 100644 --- a/hal/nxp_ls1028a.c +++ b/hal/nxp_ls1028a.c @@ -780,6 +780,8 @@ static int test_flash(void) /* Erase sector */ ret = ext_flash_erase(TEST_ADDRESS, WOLFBOOT_SECTOR_SIZE); wolfBoot_printf("Erase Sector: Ret %d\n", ret); + if (ret != 0) + return -1; /* Write Pages */ for (i=0; i Date: Thu, 24 Sep 2026 12:03:20 +0200 Subject: [PATCH 42/50] F-12886: reject non-positive length in HiFive1 flash write The write loop cast len to uint32_t, so a negative length became a huge iteration count and read past the source buffer; a zero length is a no-op success, matching the erase side. --- hal/hifive1.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hal/hifive1.c b/hal/hifive1.c index 508873f251..9948d05952 100644 --- a/hal/hifive1.c +++ b/hal/hifive1.c @@ -485,6 +485,10 @@ int RAMFUNCTION hal_flash_write(uint32_t address, const uint8_t *data, int len) uint8_t data_copy[FLASH_PAGE_SIZE]; int swmode = 0; + if (len < 0) + return -1; + if (len == 0) + return 0; if (address >= FLASH_BASE) address -= FLASH_BASE; From 7a87f671c705ae74ad13f2ab37b590f1962c4037 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 12:03:20 +0200 Subject: [PATCH 43/50] F-13607: use pointer comparisons in the store size guards Truncating the header offset to uint32_t left the upper bits of a 64-bit difference in the arithmetic; compare the header pointer against the vault bounds directly and keep the offset width-correct. --- src/pkcs11_store.c | 9 ++++++--- src/psa_store.c | 10 +++++++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/pkcs11_store.c b/src/pkcs11_store.c index 275de87b1e..a1ee90fc25 100644 --- a/src/pkcs11_store.c +++ b/src/pkcs11_store.c @@ -617,14 +617,17 @@ static struct obj_hdr *create_object(int32_t type, uint32_t tok_id, uint32_t obj static void update_store_size(struct obj_hdr *hdr, uint32_t size) { - uint32_t off; + uintptr_t off; + uint8_t *h = (uint8_t *)hdr; uint8_t *s0; struct obj_hdr *hdr_mem; - off = (uint32_t)((uint8_t *)hdr - vault_base); - if (off > WOLFBOOT_SECTOR_SIZE - (uint32_t)sizeof(struct obj_hdr)) { + if (h < vault_base || + h + sizeof(struct obj_hdr) > + vault_base + WOLFBOOT_SECTOR_SIZE) { return; } + off = (uintptr_t)(h - vault_base); s0 = cache_get_sector(0); hdr_mem = (struct obj_hdr *)(s0 + off); hdr_mem->size = size; diff --git a/src/psa_store.c b/src/psa_store.c index a310472ba7..0eca425530 100644 --- a/src/psa_store.c +++ b/src/psa_store.c @@ -355,11 +355,15 @@ static struct obj_hdr *create_object(int32_t type, uint32_t tok_id, uint32_t obj static void update_store_size(struct obj_hdr *hdr, uint32_t size) { - uint32_t off; + uintptr_t off; + uint8_t *h = (uint8_t *)hdr; struct obj_hdr *hdr_mem; - off = (uint32_t)((uint8_t *)hdr - vault_base); - if (off > WOLFBOOT_SECTOR_SIZE - (uint32_t)sizeof(struct obj_hdr)) + + if (h < vault_base || + h + sizeof(struct obj_hdr) > + vault_base + WOLFBOOT_SECTOR_SIZE) return; + off = (uintptr_t)(h - vault_base); check_vault(); memcpy(cached_sector, vault_base, WOLFBOOT_SECTOR_SIZE); hdr_mem = (struct obj_hdr *)(cached_sector + off); From 66327766cab488c8a568e12717d1ec2237e704f7 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 12:03:28 +0200 Subject: [PATCH 44/50] F-12884: drop the unused variable in the nrf54l write test --- tools/unit-tests/unit-nrf54l-uart-write.c | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/unit-tests/unit-nrf54l-uart-write.c b/tools/unit-tests/unit-nrf54l-uart-write.c index 82edf5b0aa..a8b3f47de2 100644 --- a/tools/unit-tests/unit-nrf54l-uart-write.c +++ b/tools/unit-tests/unit-nrf54l-uart-write.c @@ -108,7 +108,6 @@ START_TEST(test_write_newline_expansion) char in[200]; uint8_t exp[400]; int explen; - int i; memset(in, '\n', sizeof(in)); explen = build_expected(in, 200, exp); From 2f73f50f5997fe862b1bbe542ccaf31649b53180 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 12:03:28 +0200 Subject: [PATCH 45/50] F-12885: reset the LCR counter in the no-fork x86 uart tests The Check fixture runs once per case, so under CK_FORK=no the rejection tests saw LCR writes from the earlier tests and failed. --- tools/unit-tests/unit-x86-uart.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/unit-tests/unit-x86-uart.c b/tools/unit-tests/unit-x86-uart.c index 9f367d3634..dc877a3252 100644 --- a/tools/unit-tests/unit-x86-uart.c +++ b/tools/unit-tests/unit-x86-uart.c @@ -92,9 +92,12 @@ START_TEST(test_uart_init_parity_and_stops) } END_TEST -/* Stop counts other than 1 and 2 are not representable in the LCR. */ +/* Stop counts other than 1 and 2 are not representable in the LCR. + * The fixture runs once per case, so reset the write counter here for + * the no-fork (ASAN) run where earlier tests already wrote the LCR. */ START_TEST(test_uart_init_bad_stop_count) { + lcr_writes = 0; ck_assert_int_eq(uart_init(115200, 8, 'N', 0), -1); ck_assert_int_eq(uart_init(115200, 8, 'N', 3), -1); ck_assert_int_eq(lcr_writes, 0); @@ -104,6 +107,7 @@ END_TEST /* Existing rejections must be untouched. */ START_TEST(test_uart_init_bad_args) { + lcr_writes = 0; ck_assert_int_eq(uart_init(0, 8, 'N', 1), -1); ck_assert_int_eq(uart_init(115200, 9, 'N', 1), -1); ck_assert_int_eq(uart_init(115200, 8, 'X', 1), -1); From 815c4616c4a2ddc86c4541347cf8f6882450536a Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 12:03:28 +0200 Subject: [PATCH 46/50] F-11023: add the STM32G0 copy of the fast-path write test The G0 and G4 files carry the same double-word fast path and were fixed in the same commit, but only the G4 copy had a regression test. Clone the G4 harness with the G0 register set (BSY1, 4-bit W1C clear). --- tools/unit-tests/Makefile | 13 +- tools/unit-tests/unit-stm32g0-write.c | 223 ++++++++++++++++++++++++++ 2 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 tools/unit-tests/unit-stm32g0-write.c diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index ade84ef0dc..d0c843763c 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -148,6 +148,7 @@ TESTS+=unit-aurix-erased-fill-invert TESTS+=unit-t2080-fman-loader TESTS+=unit-ecc-raw-der TESTS+=unit-stm32g4-write +TESTS+=unit-stm32g0-write TESTS+=unit-stm32l4-write TESTS+=unit-stm32wb-write TESTS+=unit-stm32l5-write @@ -1428,6 +1429,16 @@ unit-stm32u3-flash-write: unit-stm32u3-flash-write.c stm32u3_flash_write_extract unit-stm32g4-write: unit-stm32g4-write.c stm32g4_write_extract.h gcc -o $@ unit-stm32g4-write.c $(CFLAGS) $(LDFLAGS) +# unit-stm32g0-write: the G0 copy of the F-11023 double-word fast path; +# same harness as unit-stm32g4-write. +stm32g0_write_extract.h: ../../hal/stm32g0.c + sed -n '/^static RAMFUNCTION void flash_wait_complete/,/^}/p' $< > $@ + sed -n '/^static void RAMFUNCTION flash_clear_errors/,/^}/p' $< >> $@ + sed -n '/^int RAMFUNCTION hal_flash_write/,/^}/p' $< >> $@ + +unit-stm32g0-write: unit-stm32g0-write.c stm32g0_write_extract.h + gcc -o $@ unit-stm32g0-write.c $(CFLAGS) $(LDFLAGS) + # unit-stm32l4-write runs the real hal_flash_write() from hal/stm32l4.c # (F-12062: the double-word fast path was selected on "len - i > 3" but # consumed eight bytes, so an aligned 4-7 byte tail over-read the @@ -1910,7 +1921,7 @@ GENERATED_SRC:=aurix_erased_extract.h \ keystore_api_extract.h keystore_emitted.c keystore_gen \ rp2350_flash_write_extract.h \ sdhci_host.c \ - stm32c0_write_extract.h stm32g4_write_extract.h stm32l4_write_extract.h \ + stm32c0_write_extract.h stm32g0_write_extract.h stm32g4_write_extract.h stm32l4_write_extract.h \ stm32l5_write_extract.h stm32u5_write_extract.h stm32wb_write_extract.h \ t10xx_flash_status_extract.h t10xx_qe_firmware_extract.h \ t2080_fman_extract.h \ diff --git a/tools/unit-tests/unit-stm32g0-write.c b/tools/unit-tests/unit-stm32g0-write.c new file mode 100644 index 0000000000..e713b44246 --- /dev/null +++ b/tools/unit-tests/unit-stm32g0-write.c @@ -0,0 +1,223 @@ +/* unit-stm32g0-write.c + * + * Regression test for F-11023 on the STM32G0 copy of the double-word + * fast path: the fast path of hal_flash_write() in hal/stm32g0.c must + * be taken only with at least eight bytes remaining, so an aligned + * 4-7 byte tail falls to the RMW branch and reads nothing past the + * caller's buffer. The G4/G0 files carry the same fast path and were + * fixed in the same commit (6025f354); this harness mirrors + * unit-stm32g4-write for the G0 copy. + * + * Same harness as the STM32L5/STM32U5 twins: extracted functions, + * registers on a host file, stale destination flash, canary after + * the source. The source buffer is 8-byte aligned so the fast-path + * alignment test on the data pointer can pass. + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#include +#include +#include +#include + +/* Host stand-in for the ARM build attribute. */ +#define RAMFUNCTION + +/* Host FLASH register file (offsets as in hal/stm32g0.c). */ +static uint32_t g_flash_regs[0x20 / sizeof(uint32_t)]; +#define FLASH_BASE ((uintptr_t)g_flash_regs) +#define FLASH_SR (*(volatile uint32_t *)(FLASH_BASE + 0x10)) +#define FLASH_CR (*(volatile uint32_t *)(FLASH_BASE + 0x14)) +#define FLASH_SR_EOP (1 << 0) +#define FLASH_SR_PROGERR (1 << 3) +#define FLASH_SR_WRPERR (1 << 4) +#define FLASH_SR_PGAERR (1 << 5) +#define FLASH_SR_SIZERR (1 << 6) +#define FLASH_SR_BSY1 (1 << 16) +#define FLASH_CR_PG (1 << 0) + +/* Destination flash: pre-filled with stale data (rewrite scenario). + * hal_flash_write() takes the address as uint32_t (32-bit MCU), so on + * the 64-bit host the flash must live at an address that fits in 32 + * bits: map it at a fixed low location. */ +#define FLASH_MEM_SZ 256 +#define FLASH_MEM_ADDR 0x10000000UL +static uint8_t *g_flash_mem; + +/* Source buffer followed by a canary: a pre-fix short write reads the + * canary and lands it in the destination flash. */ +#define DATA_SZ 64 +#define CANARY_SZ 32 +static uint8_t g_data[DATA_SZ + CANARY_SZ] __attribute__((aligned(8))); +#define g_canary (g_data + DATA_SZ) + +/* The real functions from hal/stm32g0.c (extracted by the Makefile). */ +#include "stm32g0_write_extract.h" + +static void setup(void) +{ + int i; + + memset(g_flash_regs, 0, sizeof(g_flash_regs)); + for (i = 0; i < FLASH_MEM_SZ; i++) + g_flash_mem[i] = 0x12; /* stale */ + for (i = 0; i < DATA_SZ; i++) + g_data[i] = (uint8_t)(0x30 + i); + /* 0x70..0x8F: distinct from the data bytes (0x30..0x6F), the stale + * flash fill (0x12) and the erased-value padding (0xFF), so a + * canary hit means source bytes past len were really read. */ + for (i = 0; i < CANARY_SZ; i++) + g_canary[i] = (uint8_t)(0x70 + i); +} + +static void teardown(void) +{ +} + +static int canary_in_flash(void) +{ + int i; + + for (i = 0; i < CANARY_SZ; i++) + if (memchr(g_flash_mem, g_canary[i], FLASH_MEM_SZ) != NULL) + return 1; + return 0; +} + +/* A write of 60 bytes: seven full double words, then a 4-byte tail. + * Pre-fix the tail took the fast path and programmed bytes 60..63 + * from source bytes past len. Post-fix the tail is RMW'd and nothing + * past len is read or written. */ +START_TEST(test_write_60_no_overread) +{ + int i; + + ck_assert_int_eq(hal_flash_write((uint32_t)(uintptr_t)g_flash_mem, + g_data, 60), 0); + + ck_assert_int_eq(memcmp(g_flash_mem, g_data, 60), 0); + for (i = 60; i < FLASH_MEM_SZ; i++) + ck_assert_uint_eq(g_flash_mem[i], 0x12); + ck_assert_int_eq(canary_in_flash(), 0); +} +END_TEST + +/* A write of 58 bytes: the final unit is partial (bytes 58,59 are + * outside the request); they are read back from flash and rewritten + * unchanged, and nothing past len is read. */ +START_TEST(test_write_58_partial_word_padded) +{ + int i; + + ck_assert_int_eq(hal_flash_write((uint32_t)(uintptr_t)g_flash_mem, + g_data, 58), 0); + + ck_assert_int_eq(memcmp(g_flash_mem, g_data, 58), 0); + /* word 14 (bytes 56..59): 58,59 keep their flash content */ + ck_assert_uint_eq(g_flash_mem[58], 0x12); + ck_assert_uint_eq(g_flash_mem[59], 0x12); + for (i = 60; i < FLASH_MEM_SZ; i++) + ck_assert_uint_eq(g_flash_mem[i], 0x12); + ck_assert_int_eq(canary_in_flash(), 0); +} +END_TEST + +/* A write of 3 bytes: the whole 8-byte unit is programmed, but only + * bytes 0..2 take the requested value; the rest is rewritten with + * what flash already held. */ +START_TEST(test_write_3_single_word_padded) +{ + int i; + + ck_assert_int_eq(hal_flash_write((uint32_t)(uintptr_t)g_flash_mem, + g_data, 3), 0); + + ck_assert_int_eq(memcmp(g_flash_mem, g_data, 3), 0); + /* byte 3 and the whole second word are rewritten unchanged */ + ck_assert_uint_eq(g_flash_mem[3], 0x12); + for (i = 4; i < FLASH_MEM_SZ; i++) + ck_assert_uint_eq(g_flash_mem[i], 0x12); + ck_assert_int_eq(canary_in_flash(), 0); +} +END_TEST + +/* A write of 64 bytes, a multiple of 8: the fast path is taken for + * every unit and behaves exactly as before the fix. */ +START_TEST(test_write_64_full_units) +{ + int i; + + ck_assert_int_eq(hal_flash_write((uint32_t)(uintptr_t)g_flash_mem, + g_data, 64), 0); + + ck_assert_int_eq(memcmp(g_flash_mem, g_data, 64), 0); + for (i = 64; i < FLASH_MEM_SZ; i++) + ck_assert_uint_eq(g_flash_mem[i], 0x12); +} +END_TEST + +/* A write starting at an address with mod-8 residue 1: the byte-wise + * prefix leaves i = 7, where the fast path fires with i % 4 != 0. + * Pre-fix the double word was programmed 3 bytes below the intended + * address and the last 3 bytes of the unit were never programmed. */ +START_TEST(test_write_unaligned_start) +{ + int i; + + ck_assert_int_eq(hal_flash_write((uint32_t)(uintptr_t)(g_flash_mem + 1), + g_data + 1, 16), 0); + + ck_assert_int_eq(memcmp(g_flash_mem + 1, g_data + 1, 16), 0); + ck_assert_uint_eq(g_flash_mem[0], 0x12); + for (i = 17; i < FLASH_MEM_SZ; i++) + ck_assert_uint_eq(g_flash_mem[i], 0x12); + ck_assert_int_eq(canary_in_flash(), 0); +} +END_TEST + +int main(void) +{ + int fails; + SRunner *sr; + Suite *s = suite_create("stm32g0_write"); + TCase *tc = tcase_create("stm32g0_write"); + + g_flash_mem = (uint8_t *)mmap((void *)FLASH_MEM_ADDR, FLASH_MEM_SZ, + PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, + -1, 0); + if (g_flash_mem == MAP_FAILED) + return 77; + + tcase_add_checked_fixture(tc, setup, teardown); + tcase_add_test(tc, test_write_60_no_overread); + tcase_add_test(tc, test_write_58_partial_word_padded); + tcase_add_test(tc, test_write_3_single_word_padded); + tcase_add_test(tc, test_write_64_full_units); + tcase_add_test(tc, test_write_unaligned_start); + suite_add_tcase(s, tc); + + sr = srunner_create(s); + srunner_run_all(sr, CK_NORMAL); + fails = srunner_ntests_failed(sr); + srunner_free(sr); + munmap(g_flash_mem, FLASH_MEM_SZ); + + return fails; +} From 48c937f73301091df5602d0c6824c34bc8d80088 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 12:35:41 +0200 Subject: [PATCH 47/50] F-11030: cast %p through uintptr_t in uart_vprintf The direct (unsigned long long) cast of the va_arg pointer warns pointer-to-int-cast on 32-bit targets (void* is 32 bits), which the nxp-t1024 parse-tools build rejects with -Werror. Route the cast through uintptr_t so it is size-exact on every word size. --- src/string.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/string.c b/src/string.c index ac715fb9f7..fca8beaa36 100644 --- a/src/string.c +++ b/src/string.c @@ -586,10 +586,12 @@ void uart_vprintf(const char* fmt, va_list argp) uart_writenum_ll(val, 0, 16, zeropad, maxdigits); } else if (islong == 1 || iszl || *fmtp == 'p') { - /* %lx / %zx / %p: long/pointer is 64-bit here */ + /* %lx / %zx / %p: consume at the type's natural width + * via va_arg, then widen to 64 bits for the printer */ unsigned long long val; if (*fmtp == 'p') { - val = (unsigned long long)va_arg(argp, void *); + val = (unsigned long long)(uintptr_t) + va_arg(argp, void *); } else { val = (unsigned long long)va_arg(argp, From f5c8f61ef25e38a3e0f7c44efc6b05099d04208a Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 12:35:41 +0200 Subject: [PATCH 48/50] F-13606: declare ramboot 'ret' only where the NO_XIP reads use it The version check moved to 'version' in F-13606, leaving 'ret' used only in the EXT_FLASH && NO_XIP branches. EXT_FLASH/XIP configs (e.g. raspi3-encrypted) then fail -Werror=unused-variable. --- src/update_ram.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/update_ram.c b/src/update_ram.c index a730acbbc8..026f80904d 100644 --- a/src/update_ram.c +++ b/src/update_ram.c @@ -83,10 +83,12 @@ static inline int ramboot_region_overlap(uintptr_t img_lo, uintptr_t img_hi, /* Function to load image from flash to ram */ int wolfBoot_ramboot(struct wolfBoot_image *img, uint8_t *src, uint8_t *dst) { - int ret; uint32_t img_size; uint32_t version; BENCHMARK_DECLARE(); +#if defined(EXT_FLASH) && defined(NO_XIP) + int ret; +#endif /* read header into RAM */ wolfBoot_printf("Loading header %d bytes from %p to %p\n", From 9bc15c1656aab8067bcbe2ac74ff88ab369a293e Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 12:48:08 +0200 Subject: [PATCH 49/50] fix Fenrir review: ls1028a test_flash return contract, x86 FSP stop bits - hal/nxp_ls1028a.c: ext_flash_erase/write return len on success, not 0, so the F-12067 follow-up guards (ret != 0) always tripped and test_flash could never pass; check ret < 0 - src/x86/qemu_fsp.c: the F-12885 stop-bit guard rejects the in-tree uart_init(115200, 8, 'N', 0) call, leaving the FSP QEMU debug UART unprogrammed; pass 1 stop bit --- hal/nxp_ls1028a.c | 4 ++-- src/x86/qemu_fsp.c | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hal/nxp_ls1028a.c b/hal/nxp_ls1028a.c index b329cd7620..95ac3af9b8 100644 --- a/hal/nxp_ls1028a.c +++ b/hal/nxp_ls1028a.c @@ -780,7 +780,7 @@ static int test_flash(void) /* Erase sector */ ret = ext_flash_erase(TEST_ADDRESS, WOLFBOOT_SECTOR_SIZE); wolfBoot_printf("Erase Sector: Ret %d\n", ret); - if (ret != 0) + if (ret < 0) return -1; /* Write Pages */ @@ -789,7 +789,7 @@ static int test_flash(void) } ret = ext_flash_write(TEST_ADDRESS, pageData, sizeof(pageData)); wolfBoot_printf("Write Page: Ret %d\n", ret); - if (ret != 0) + if (ret < 0) return -1; /* Read page */ diff --git a/src/x86/qemu_fsp.c b/src/x86/qemu_fsp.c index 9093a7a603..37fc5650f7 100644 --- a/src/x86/qemu_fsp.c +++ b/src/x86/qemu_fsp.c @@ -108,7 +108,7 @@ int fsp_machine_update_s_parameters(uint8_t *default_s_params) int post_temp_ram_init_cb(void) { - uart_init(115200, 8, 'N', 0); + uart_init(115200, 8, 'N', 1); return 0; } From 17e4327c518549b78a7af136129db65ae7ced59f Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 24 Sep 2026 13:23:35 +0200 Subject: [PATCH 50/50] footprint: SIGN=NONE limit 5172 -> 5180 --- tools/test.mk | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/test.mk b/tools/test.mk index 4f8ac6bc98..ebe75bb9b4 100644 --- a/tools/test.mk +++ b/tools/test.mk @@ -1238,7 +1238,9 @@ test-size-all: # Measured with the CI container (ghcr.io/wolfssl/wolfboot-ci-arm). # Re-measured after the lib/wolfssl bump to master 4aa1ad7a5, which adds # 8-136 bytes per configuration. - make test-size SIGN=NONE LIMIT=5172 NO_ARM_ASM=1 + # Re-measured 2026-09-24: SIGN=NONE +8B (5180) from the Fenrir fix batch; + # all other configurations measured smaller than their limits. + make test-size SIGN=NONE LIMIT=5180 NO_ARM_ASM=1 make keysclean make test-size SIGN=ED25519 LIMIT=12356 NO_ARM_ASM=1 make keysclean