F-3963: fix hal_flash_erase loop bound in stm32c0.c skipping final partial page

end_address = address + len - 1 (inclusive) with p < end_address (strict) skips
the final page when len is not a multiple of FLASH_PAGE_SIZE.  Change to exclusive
end_address = address + len, matching the fix applied to stm32g0/l0/wb.  Add
WOLFBOOT_UNIT_TEST_FLASH_ERASE guards so the function can be included in isolation
on the host, and add unit-flash-erase-c0 to the test suite.
pull/795/head
Daniele Lacamera 2026-06-11 18:16:48 +02:00
parent 2f8f0c6844
commit 4b95dc2474
3 changed files with 171 additions and 1 deletions

View File

@ -20,18 +20,24 @@
*/
#include <stdint.h>
#ifndef WOLFBOOT_UNIT_TEST_FLASH_ERASE
#include <image.h>
#endif
#ifndef WOLFBOOT_UNIT_TEST_FLASH_ERASE
#ifndef NVM_FLASH_WRITEONCE
# error "wolfBoot STM32C0 HAL: no WRITEONCE support detected. Please define NVM_FLASH_WRITEONCE"
#endif
#endif
/* STM32 C0 register configuration */
/* Assembly helpers */
#ifndef WOLFBOOT_UNIT_TEST_FLASH_ERASE
#define DMB() __asm__ volatile ("dmb")
#define ISB() __asm__ volatile ("isb")
#define DSB() __asm__ volatile ("dsb")
#endif /* !WOLFBOOT_UNIT_TEST_FLASH_ERASE */
/*** RCC ***/
@ -65,12 +71,14 @@
#define SYSCFG_APB2_CLOCK_ER_VAL (1 << 0) /* RM0490 - 5.4.14 - RCC_APBENR2 - SYSCFGEN */
#define FLASH_BASE (0x40022000) /*FLASH_R_BASE = 0x40000000UL + 0x00020000UL + 0x00002000UL */
#ifndef WOLFBOOT_UNIT_TEST_FLASH_ERASE
#define FLASH_ACR (*(volatile uint32_t *)(FLASH_BASE + 0x00)) /* RM0490 - 3.7.1 - FLASH_ACR */
#define FLASH_KEY (*(volatile uint32_t *)(FLASH_BASE + 0x08)) /* RM0490 - 3.7.2 - FLASH_KEYR */
#define FLASH_OPTKEY (*(volatile uint32_t *)(FLASH_BASE + 0x0C)) /* RM0490 - 3.7.3 - FLASH_OPTKEYR */
#define FLASH_SR (*(volatile uint32_t *)(FLASH_BASE + 0x10)) /* RM0490 - 3.7.4 - FLASH_SR */
#define FLASH_CR (*(volatile uint32_t *)(FLASH_BASE + 0x14)) /* RM0490 - 3.7.5 - FLASH_CR */
#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 */
@ -108,6 +116,7 @@
#define FLASH_OPTKEY2 (0x4C5D6E7F)
#ifndef WOLFBOOT_UNIT_TEST_FLASH_ERASE
static void RAMFUNCTION flash_set_waitstates(unsigned int waitstates)
{
uint32_t reg, mask_val, set_val;
@ -191,6 +200,7 @@ void RAMFUNCTION hal_flash_lock(void)
if ((FLASH_CR & FLASH_CR_LOCK) == 0)
FLASH_CR |= FLASH_CR_LOCK;
}
#endif /* !WOLFBOOT_UNIT_TEST_FLASH_ERASE */
int RAMFUNCTION hal_flash_erase(uint32_t address, int len)
@ -199,7 +209,7 @@ int RAMFUNCTION hal_flash_erase(uint32_t address, int len)
uint32_t p;
if (len == 0)
return -1;
end_address = address + len - 1;
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;
@ -211,6 +221,7 @@ int RAMFUNCTION hal_flash_erase(uint32_t address, int len)
return 0;
}
#ifndef WOLFBOOT_UNIT_TEST_FLASH_ERASE
static void clock_pll_off(void)
{
uint32_t reg32;
@ -308,3 +319,4 @@ void RAMFUNCTION hal_prepare_boot(void)
do_secure_boot();
#endif
}
#endif /* !WOLFBOOT_UNIT_TEST_FLASH_ERASE */

View File

@ -66,6 +66,7 @@ TESTS+=unit-flash-erase-h7
TESTS+=unit-flash-erase-wb
TESTS+=unit-flash-erase-l0
TESTS+=unit-flash-erase-g0
TESTS+=unit-flash-erase-c0
TESTS+=unit-otp-keystore
TESTS+=unit-x86-paging-oob
TESTS+=unit-fwtpm-nv-oob
@ -303,6 +304,11 @@ unit-flash-erase-l0: unit-flash-erase-l0.c ../../hal/stm32l0.c
unit-flash-erase-g0: unit-flash-erase-g0.c ../../hal/stm32g0.c
gcc -o $@ unit-flash-erase-g0.c $(CFLAGS) $(LDFLAGS)
# unit-flash-erase-c0 includes hal/stm32c0.c directly (guarded to hal_flash_erase
# via WOLFBOOT_UNIT_TEST_FLASH_ERASE), so stm32c0.c is not a separate input.
unit-flash-erase-c0: unit-flash-erase-c0.c ../../hal/stm32c0.c
gcc -o $@ unit-flash-erase-c0.c $(CFLAGS) $(LDFLAGS)
# unit-otp-keystore includes src/flash_otp_keystore.c directly (guarded to its
# host-portable code via WOLFBOOT_UNIT_TEST_OTP_KEYSTORE), so it is not a
# separate input.

View File

@ -0,0 +1,152 @@
/* unit-flash-erase-c0.c
*
* Unit tests for the page-loop bound in hal_flash_erase() (hal/stm32c0.c).
* Regression for F-3963: end_address = address + len - 1 (inclusive) combined
* with p < end_address (strict) skips the final page when len % PAGE_SIZE != 0.
*
*
* 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 <check.h>
#include <stdint.h>
#include <stdio.h>
/* hal/stm32c0.c is tightly coupled to STM32C0 hardware registers.
* Compile only hal_flash_erase() in isolation by defining this guard;
* all other functions and hardware macros are excluded and replaced below. */
#define WOLFBOOT_UNIT_TEST_FLASH_ERASE
/* RAMFUNCTION must be empty on the host */
#define RAMFUNCTION
/* DMB is a no-op on the host (stm32c0 DMB fires before STRT is written, so
* we capture from flash_wait_complete instead). */
#define DMB() /* nothing */
/* Mocked flash control/status registers */
static uint32_t mock_FLASH_CR;
static uint32_t mock_FLASH_SR;
#define FLASH_CR mock_FLASH_CR
#define FLASH_SR mock_FLASH_SR
/* Constants from hal/stm32c0.c (must mirror exactly). */
#define FLASH_CR_STRT (1 << 16)
#define FLASH_CR_PER (1 << 1)
#define FLASH_CR_PNB_SHIFT 3
#define FLASH_CR_PNB_MASK 0x7f
/* Record the FLASH_CR value on each page-erase command (captured at the
* start of flash_wait_complete, when STRT has just been written). */
#define ERASE_LOG_MAX 64
static uint32_t erase_cr[ERASE_LOG_MAX];
static int erase_log_n;
/* Stubs for functions called by hal_flash_erase().
* flash_wait_complete is called immediately after FLASH_CR |= FLASH_CR_STRT,
* so it is the right place to capture the current register state.
* It clears STRT to mirror what real hardware does automatically. */
static void flash_wait_complete(void)
{
if ((mock_FLASH_CR & FLASH_CR_STRT) && erase_log_n < ERASE_LOG_MAX) {
erase_cr[erase_log_n] = mock_FLASH_CR;
erase_log_n++;
}
mock_FLASH_CR &= ~FLASH_CR_STRT;
}
static void flash_clear_errors(void) {}
#include "../../hal/stm32c0.c"
/* Decode the page number field from a captured FLASH_CR value. */
static uint32_t page_of(uint32_t cr)
{
return (cr >> FLASH_CR_PNB_SHIFT) & FLASH_CR_PNB_MASK;
}
static void reset_mocks(void)
{
mock_FLASH_CR = 0;
mock_FLASH_SR = 0;
erase_log_n = 0;
}
/* Erasing exactly one page must issue exactly one erase command. */
START_TEST(test_erase_single_page_aligned)
{
reset_mocks();
hal_flash_erase(0x08000000UL, FLASH_PAGE_SIZE);
ck_assert_int_eq(erase_log_n, 1);
ck_assert_uint_eq(page_of(erase_cr[0]), 0);
}
END_TEST
/* Erasing two full pages must issue exactly two erase commands. */
START_TEST(test_erase_two_pages_aligned)
{
reset_mocks();
hal_flash_erase(0x08000000UL, 2 * FLASH_PAGE_SIZE);
ck_assert_int_eq(erase_log_n, 2);
ck_assert_uint_eq(page_of(erase_cr[0]), 0);
ck_assert_uint_eq(page_of(erase_cr[1]), 1);
}
END_TEST
/* Regression for F-3963: len = PAGE_SIZE + 1 spans two pages and both must be
* erased. Before the fix, end_address = address + len - 1 landed exactly on
* the start of page 1, so the strict `p < end_address` guard excluded it. */
START_TEST(test_erase_unaligned_len_covers_last_page)
{
reset_mocks();
/* len = 0x801: bytes [0..0x800], crossing into page 1 */
hal_flash_erase(0x08000000UL, FLASH_PAGE_SIZE + 1);
ck_assert_int_eq(erase_log_n, 2);
ck_assert_uint_eq(page_of(erase_cr[0]), 0);
ck_assert_uint_eq(page_of(erase_cr[1]), 1);
}
END_TEST
Suite *flash_erase_c0_suite(void)
{
Suite *s = suite_create("flash-erase-c0");
TCase *tc = tcase_create("flash-erase-c0");
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);
suite_add_tcase(s, tc);
return s;
}
int main(void)
{
int fails;
Suite *s = flash_erase_c0_suite();
SRunner *sr = srunner_create(s);
srunner_run_all(sr, CK_NORMAL);
fails = srunner_ntests_failed(sr);
srunner_free(sr);
return fails;
}