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 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() diff --git a/hal/hifive1.c b/hal/hifive1.c index a40e670626..9948d05952 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) @@ -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; @@ -573,8 +577,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; 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/hal/mcxa.c b/hal/mcxa.c index 1f819103a3..ec38026609 100644 --- a/hal/mcxa.c +++ b/hal/mcxa.c @@ -120,9 +120,14 @@ void RAMFUNCTION hal_flash_lock(void) int RAMFUNCTION hal_flash_erase(uint32_t address, int len) { + if (len <= 0) + return -1; while ((address % 4) != 0) - address --; - if (FLASH_EraseSector(&pflash, address, len, kFLASH_ApiEraseKey) != kStatus_Success) + 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/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/hal/nxp_ls1028a.c b/hal/nxp_ls1028a.c index 4e29ca38e6..95ac3af9b8 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= 6 * 256 + 512) ? 1 : -1]; + /* 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] @@ -433,9 +441,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 +486,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) { @@ -485,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) { 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/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); 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/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) 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/hal/stm32h7.c b/hal/stm32h7.c index 4383e4daa7..83e8cd6c9a 100644 --- a/hal/stm32h7.c +++ b/hal/stm32h7.c @@ -90,7 +90,34 @@ 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, 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). 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; + + 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/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/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/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/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/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; } } diff --git a/src/image.c b/src/image.c index ee23840c33..aaec19c612 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. */ @@ -2216,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 */ @@ -2240,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) { @@ -2253,10 +2257,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); @@ -2266,10 +2267,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); @@ -2280,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; } @@ -2300,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; + } } } @@ -2313,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) { @@ -2370,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"); @@ -2380,8 +2389,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; } @@ -2395,7 +2406,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; + } } } @@ -2427,7 +2442,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; + } } 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; diff --git a/src/pkcs11_store.c b/src/pkcs11_store.c index 5ad3dde1e7..a1ee90fc25 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)) { @@ -613,15 +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; - if (((uint8_t *)hdr) < vault_base || - ((uint8_t *)hdr > vault_base + WOLFBOOT_SECTOR_SIZE)) { + if (h < vault_base || + h + sizeof(struct obj_hdr) > + vault_base + WOLFBOOT_SECTOR_SIZE) { return; } - off = (uintptr_t)hdr - (uintptr_t)vault_base; + off = (uintptr_t)(h - vault_base); s0 = cache_get_sector(0); hdr_mem = (struct obj_hdr *)(s0 + off); hdr_mem->size = size; @@ -774,11 +780,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 8f95e965aa..0eca425530 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. */ @@ -351,13 +355,16 @@ 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; - if (((uint8_t *)hdr) < vault_base || - ((uint8_t *)hdr > vault_base + WOLFBOOT_SECTOR_SIZE)) + + if (h < vault_base || + h + sizeof(struct obj_hdr) > + vault_base + WOLFBOOT_SECTOR_SIZE) return; + off = (uintptr_t)(h - vault_base); 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/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; diff --git a/src/string.c b/src/string.c index bf31b5669c..fca8beaa36 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 == '-') { @@ -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') { @@ -500,7 +505,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 +544,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 +585,30 @@ 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: 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)(uintptr_t) + 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/src/ubootenv.c b/src/ubootenv.c index 4e61bf5578..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; @@ -310,6 +312,13 @@ 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. 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 n; } @@ -357,7 +366,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/src/update_ram.c b/src/update_ram.c index 3b935fe476..026f80904d 100644 --- a/src/update_ram.c +++ b/src/update_ram.c @@ -83,9 +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", @@ -101,8 +104,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; } @@ -164,8 +167,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/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; } 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); 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() 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 diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index 1d9d80b84d..d0c843763c 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 @@ -106,6 +107,11 @@ 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-x86-uart +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 TESTS+=unit-dice-token-size @@ -116,6 +122,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 @@ -129,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 @@ -140,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 @@ -153,6 +162,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 @@ -682,6 +692,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) @@ -1173,6 +1189,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 @@ -1368,9 +1395,50 @@ 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-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 + 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-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) +# 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 @@ -1559,6 +1627,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 @@ -1725,6 +1802,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). @@ -1800,6 +1891,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) @@ -1824,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/mcxa_fsl_stub/fsl_common.h b/tools/unit-tests/mcxa_fsl_stub/fsl_common.h index bce2daa2ec..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 { - int dummy; + 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 bd864a6abb..e5c107a103 100644 --- a/tools/unit-tests/mcxa_fsl_stub/fsl_romapi.h +++ b/tools/unit-tests/mcxa_fsl_stub/fsl_romapi.h @@ -1,13 +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 len, uint32_t key); + uint32_t length_in_bytes, uint32_t key); #define kFLASH_ApiEraseKey 0x6b65796b 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; +} 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; +} 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; 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; +} diff --git a/tools/unit-tests/unit-flash-write-mcxa.c b/tools/unit-tests/unit-flash-write-mcxa.c index b98c2606bd..a97d2559ca 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_len; + status_t FLASH_ProgramPhrase(flash_config_t *config, uint32_t start, uint8_t *src, uint32_t len) { @@ -58,9 +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 len, uint32_t key) + uint32_t length_in_bytes, uint32_t key) { - (void)config; (void)start; (void)len; (void)key; + (void)config; (void)key; + erase_calls++; + last_erase_start = start; + last_erase_len = length_in_bytes; return kStatus_Success; } @@ -80,6 +88,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_len = 0; } static void teardown(void) @@ -161,6 +172,42 @@ START_TEST(test_aligned_write_bulk_then_tail) } END_TEST +/* 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_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_len, 8192); +} +END_TEST + +/* A zero-length erase request must be rejected without touching the ROM + * API. */ +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 +217,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_passes_byte_length); + tcase_add_test(tc, test_erase_zero_len); + tcase_add_test(tc, test_erase_negative_len); suite_add_tcase(s, tc); return s; 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); 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; +} 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..a8b3f47de2 --- /dev/null +++ b/tools/unit-tests/unit-nrf54l-uart-write.c @@ -0,0 +1,186 @@ +/* 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; + + 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; +} diff --git a/tools/unit-tests/unit-pkcs11_store.c b/tools/unit-tests/unit-pkcs11_store.c index 186ffbf990..e03da28511 100644 --- a/tools/unit-tests/unit-pkcs11_store.c +++ b/tools/unit-tests/unit-pkcs11_store.c @@ -433,6 +433,117 @@ 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 + +/* 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; @@ -951,7 +1062,11 @@ 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_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 0823568212..6ae6cf0f00 100644 --- a/tools/unit-tests/unit-psa_store.c +++ b/tools/unit-tests/unit-psa_store.c @@ -192,6 +192,70 @@ 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 + +/* 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 }; @@ -438,7 +502,10 @@ 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_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); 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; +} 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; 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..20917340a3 --- /dev/null +++ b/tools/unit-tests/unit-stm32h7-flash-write.c @@ -0,0 +1,168 @@ +/* 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. Addresses are absolute, as passed by callers + * (FLASHMEM_ADDRESS_SPACE based); the split point is FLASH_BANK2_BASE. + * + * 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 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). */ +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 - 16, data, 32), 0); + ck_assert_int_eq(g_calls, 2); + 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); + 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(FLASHMEM_ADDRESS_SPACE + 0x400, data, 16), + 0); + ck_assert_int_eq(g_calls, 1); + ck_assert_uint_eq(g_addr[0], FLASHMEM_ADDRESS_SPACE + 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 + 0x400, data, 16), 0); + ck_assert_int_eq(g_calls, 1); + ck_assert_uint_eq(g_addr[0], FLASH_BANK2_BASE + 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 - 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; +} 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; +} diff --git a/tools/unit-tests/unit-string.c b/tools/unit-tests/unit-string.c index 3ca21f3dda..0f32d63adc 100644 --- a/tools/unit-tests/unit-string.c +++ b/tools/unit-tests/unit-string.c @@ -454,6 +454,59 @@ 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 + +/* 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"); @@ -488,6 +541,8 @@ 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); + tcase_add_test(tcase_misc, test_uart_printf_negative_star_width); suite_add_tcase(s, tcase_strncasecmp); suite_add_tcase(s, tcase_misc); 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; +} diff --git a/tools/unit-tests/unit-ubootenv.c b/tools/unit-tests/unit-ubootenv.c index 0953a4e245..c8a4a411e3 100644 --- a/tools/unit-tests/unit-ubootenv.c +++ b/tools/unit-tests/unit-ubootenv.c @@ -373,6 +373,46 @@ 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 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" + "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, 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, 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, UBOOT_ENV_VAL_MAX - 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 +439,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; diff --git a/tools/unit-tests/unit-update-ram.c b/tools/unit-tests/unit-update-ram.c index 62ad476a4a..65df44cb09 100644 --- a/tools/unit-tests/unit-update-ram.c +++ b/tools/unit-tests/unit-update-ram.c @@ -287,6 +287,52 @@ 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; + 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 +652,8 @@ 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 = tcase_create("Sunny day test with no update available"); @@ -636,6 +684,8 @@ 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); tcase_add_test(forward_update_samesize, test_forward_update_samesize); @@ -660,6 +710,8 @@ 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); suite_add_tcase(s, forward_update_samesize); @@ -683,6 +735,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); diff --git a/tools/unit-tests/unit-x86-uart.c b/tools/unit-tests/unit-x86-uart.c new file mode 100644 index 0000000000..dc877a3252 --- /dev/null +++ b/tools/unit-tests/unit-x86-uart.c @@ -0,0 +1,139 @@ +/* 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. + * 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); +} +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); + 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; +}