F-3965: fix hal_flash_erase loop bound in stm32l0.c skipping final partial page

end_address was computed as address + len - 1 (inclusive) but paired with a
strict p < end_address guard, so the last page was skipped whenever len was
not a multiple of FLASH_PAGE_SIZE.  Change end_address to the exclusive form
address + len so the loop covers all pages that overlap the requested range.

Add unit-flash-erase-l0 regression test (mirrors the unit-flash-erase-wb
harness) with guards in hal/stm32l0.c allowing the HAL to be compiled on the
host; the unaligned-len test fails before the fix and passes after.
pull/795/head
Daniele Lacamera 2026-06-11 18:08:22 +02:00
parent 2f53d0d1d9
commit 15e4b81925
3 changed files with 156 additions and 2 deletions

View File

@ -20,11 +20,15 @@
*/
#include <stdint.h>
#ifndef WOLFBOOT_UNIT_TEST_FLASH_ERASE
#include <image.h>
#endif
/* STM32 L0 register configuration */
/* Assembly helpers */
#ifndef WOLFBOOT_UNIT_TEST_FLASH_ERASE
#define DMB() __asm__ volatile ("dmb")
#endif
/*** RCC ***/
@ -49,11 +53,13 @@
#define PWR_APB1_CLOCK_ER_VAL (1 << 28)
#define FLASH_BASE (0x40022000)
#define FLASH_ACR (*(volatile uint32_t *)(FLASH_BASE + 0x00))
#ifndef WOLFBOOT_UNIT_TEST_FLASH_ERASE
#define FLASH_PECR (*(volatile uint32_t *)(FLASH_BASE + 0x04))
#define FLASH_PEKEY (*(volatile uint32_t *)(FLASH_BASE + 0x0c))
#define FLASH_PRGKEY (*(volatile uint32_t *)(FLASH_BASE + 0x10))
#define FLASH_SR (*(volatile uint32_t *)(FLASH_BASE + 0x18))
#define FLASHMEM_ADDRESS_SPACE (0x08000000)
#endif /* !WOLFBOOT_UNIT_TEST_FLASH_ERASE */
#define FLASH_PAGE_SIZE (128)
/* Register values */
@ -73,6 +79,7 @@
#define FLASH_PECR_ERASE (1 << 9)
#ifndef WOLFBOOT_UNIT_TEST_FLASH_ERASE
static void RAMFUNCTION flash_set_waitstates(unsigned int waitstates)
{
if (waitstates && ((FLASH_ACR & 1) == 0))
@ -82,6 +89,7 @@ static void RAMFUNCTION flash_set_waitstates(unsigned int waitstates)
while ((FLASH_ACR & 1) != waitstates)
;
}
#endif /* !WOLFBOOT_UNIT_TEST_FLASH_ERASE */
static RAMFUNCTION void flash_wait_complete(void)
{
@ -94,6 +102,7 @@ static void RAMFUNCTION clear_errors(void)
FLASH_SR |= ( FLASH_SR_SIZERR | FLASH_SR_PGAERR | FLASH_SR_WRPERR | FLASH_SR_EOP );
}
#ifndef WOLFBOOT_UNIT_TEST_FLASH_ERASE
int RAMFUNCTION hal_flash_write(uint32_t address, const uint8_t *data, int len)
{
int i = 0;
@ -151,6 +160,7 @@ void RAMFUNCTION hal_flash_lock(void)
if ((FLASH_PECR & FLASH_PECR_PRGLOCK) == 0)
FLASH_PECR |= FLASH_PECR_PRGLOCK;
}
#endif /* !WOLFBOOT_UNIT_TEST_FLASH_ERASE */
int RAMFUNCTION hal_flash_erase(uint32_t address, int len)
@ -159,7 +169,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) {
FLASH_PECR |= FLASH_PECR_PROG | FLASH_PECR_ERASE;
*(volatile uint32_t *)(p + FLASHMEM_ADDRESS_SPACE) = 0xFFFFFFFF;
@ -169,6 +179,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;
@ -270,4 +281,4 @@ void hal_prepare_boot(void)
clock_pll_off();
#endif
}
#endif /* !WOLFBOOT_UNIT_TEST_FLASH_ERASE */

View File

@ -64,6 +64,7 @@ TESTS+=unit-fit-fpga
TESTS+=unit-mpusize
TESTS+=unit-flash-erase-h7
TESTS+=unit-flash-erase-wb
TESTS+=unit-flash-erase-l0
TESTS+=unit-otp-keystore
TESTS+=unit-x86-paging-oob
TESTS+=unit-fwtpm-nv-oob
@ -291,6 +292,11 @@ unit-flash-erase-h7: unit-flash-erase-h7.c ../../hal/stm32h7.c
unit-flash-erase-wb: unit-flash-erase-wb.c ../../hal/stm32wb.c
gcc -o $@ unit-flash-erase-wb.c $(CFLAGS) $(LDFLAGS)
# unit-flash-erase-l0 includes hal/stm32l0.c directly (guarded to hal_flash_erase
# via WOLFBOOT_UNIT_TEST_FLASH_ERASE), so stm32l0.c is not a separate input.
unit-flash-erase-l0: unit-flash-erase-l0.c ../../hal/stm32l0.c
gcc -o $@ unit-flash-erase-l0.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,137 @@
/* unit-flash-erase-l0.c
*
* Unit tests for the page-loop bound in hal_flash_erase() (hal/stm32l0.c).
* Regression for F-3965: 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 <string.h>
/* hal/stm32l0.c is tightly coupled to STM32L0 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 */
#define DMB() /* nothing */
/* Mocked flash control/status registers */
static uint32_t mock_FLASH_PECR;
static uint32_t mock_FLASH_SR;
#define FLASH_PECR mock_FLASH_PECR
#define FLASH_SR mock_FLASH_SR
/* Mock flash memory: 4 pages of FLASH_PAGE_SIZE (128) bytes each.
* hal_flash_erase writes 0xFFFFFFFF to the first word of each erased page via:
* *(volatile uint32_t *)(p + FLASHMEM_ADDRESS_SPACE) = 0xFFFFFFFF
* Redirect that write into this buffer by making FLASHMEM_ADDRESS_SPACE the
* buffer's base address and calling hal_flash_erase with address = 0. */
#define MOCK_FLASH_PAGES 4
#define MOCK_FLASH_SIZE (MOCK_FLASH_PAGES * 128)
static uint8_t mock_flash_mem[MOCK_FLASH_SIZE];
#define FLASHMEM_ADDRESS_SPACE ((uintptr_t)mock_flash_mem)
#include "../../hal/stm32l0.c"
static void reset_mocks(void)
{
mock_FLASH_PECR = 0;
mock_FLASH_SR = 0;
memset(mock_flash_mem, 0xAA, sizeof(mock_flash_mem));
}
/* Return 1 if the page at index `page` had its first word written to 0xFFFFFFFF. */
static int page_erased(int page)
{
uint32_t word;
memcpy(&word, &mock_flash_mem[page * FLASH_PAGE_SIZE], sizeof(word));
return word == 0xFFFFFFFFu;
}
/* Erasing exactly one page must erase that page and nothing beyond it. */
START_TEST(test_erase_single_page_aligned)
{
reset_mocks();
hal_flash_erase(0, FLASH_PAGE_SIZE);
ck_assert_int_eq(page_erased(0), 1);
ck_assert_int_eq(page_erased(1), 0);
}
END_TEST
/* Erasing two full pages must erase both and not touch the third. */
START_TEST(test_erase_two_pages_aligned)
{
reset_mocks();
hal_flash_erase(0, 2 * FLASH_PAGE_SIZE);
ck_assert_int_eq(page_erased(0), 1);
ck_assert_int_eq(page_erased(1), 1);
ck_assert_int_eq(page_erased(2), 0);
}
END_TEST
/* Regression for F-3965: len = PAGE_SIZE + 1 spans two pages; both must be
* erased. Before the fix, end_address = address + len - 1 landed exactly on
* the first byte of page 1, so the strict `p < end_address` guard excluded it. */
START_TEST(test_erase_unaligned_len_covers_last_page)
{
reset_mocks();
/* len = 129: bytes [0..128], crossing into page 1 */
hal_flash_erase(0, FLASH_PAGE_SIZE + 1);
ck_assert_int_eq(page_erased(0), 1);
ck_assert_int_eq(page_erased(1), 1);
ck_assert_int_eq(page_erased(2), 0);
}
END_TEST
Suite *flash_erase_l0_suite(void)
{
Suite *s = suite_create("flash-erase-l0");
TCase *tc = tcase_create("flash-erase-l0");
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_l0_suite();
SRunner *sr = srunner_create(s);
srunner_run_all(sr, CK_NORMAL);
fails = srunner_ntests_failed(sr);
srunner_free(sr);
return fails;
}