diff --git a/.gitignore b/.gitignore index a75fcf7e9a..539fdcd240 100644 --- a/.gitignore +++ b/.gitignore @@ -182,14 +182,23 @@ tools/unit-tests/unit-policy-create tools/unit-tests/unit-sign-encrypted-output tools/unit-tests/unit-update-flash-delta tools/unit-tests/unit-update-flash-self-update +tools/unit-tests/unit-update-flash-hook tools/unit-tests/unit-loader-tpm-init tools/unit-tests/unit-update-ram-nofixed +tools/unit-tests/unit-uart-flash tools/unit-tests/unit-max-space tools/unit-tests/unit-sdhci-disk-unaligned tools/unit-tests/unit-fwtpm-stub tools/unit-tests/unit-gzip +tools/unit-tests/unit-fit-gzip +tools/unit-tests/unit-fit-nogzip +tools/unit-tests/unit-flash-erase-h7 +tools/unit-tests/unit-keygen-xmss-params tools/unit-tests/unit-linux-loader-e820 tools/unit-tests/unit-linux-loader-syssize +tools/unit-tests/unit-mpusize +tools/unit-tests/unit-otp-keystore +tools/unit-tests/unit-tpm-api-names # Elf preprocessing tools @@ -197,6 +206,7 @@ tools/squashelf/** !tools/squashelf/squashelf.c !tools/squashelf/Makefile !tools/squashelf/README.md +!tools/squashelf/test-range-overflow.py # Generated configuration files .config diff --git a/hal/imx_rt.c b/hal/imx_rt.c index b89d93f214..dc5a62fd61 100644 --- a/hal/imx_rt.c +++ b/hal/imx_rt.c @@ -936,7 +936,16 @@ int RAMFUNCTION hal_flash_write(uint32_t address, const uint8_t *data, int len) asm volatile("cpsid i"); int write_success = 0; for (i = 0; i < len; i+= CONFIG_FLASH_PAGE_SIZE) { - memcpy(wbuf, data + i, CONFIG_FLASH_PAGE_SIZE); + /* The NOR program command always writes a full page. Bound the copy + * to the bytes actually provided by the caller so sub-page writes + * (e.g. the 1-byte trailer updates) do not overread the source buffer. + * Pad the remainder of the page with the erased value (0xFF), which is + * a no-op for NOR programming and preserves existing flash contents. */ + int chunk = len - i; + if (chunk > (int)CONFIG_FLASH_PAGE_SIZE) + chunk = (int)CONFIG_FLASH_PAGE_SIZE; + memset(wbuf, 0xFF, sizeof(wbuf)); + memcpy(wbuf, data + i, chunk); status = g_bootloaderTree->flexSpiNorDriver->program( CONFIG_FLASH_FLEXSPI_INSTANCE, FLEXSPI_CONFIG, diff --git a/hal/stm32h7.c b/hal/stm32h7.c index c61345a169..71e9ad53c2 100644 --- a/hal/stm32h7.c +++ b/hal/stm32h7.c @@ -19,8 +19,11 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA */ +#ifndef WOLFBOOT_UNIT_TEST_FLASH_ERASE #include "hal/stm32h7.h" +#endif +#ifndef WOLFBOOT_UNIT_TEST_FLASH_ERASE static uint32_t stm32h7_cache[STM32H7_WORD_SIZE / sizeof(uint32_t)]; #define FLASH_BANK_1 0 @@ -194,6 +197,7 @@ void RAMFUNCTION hal_flash_lock(void) if ((FLASH_CR2 & FLASH_CR_LOCK) == 0) FLASH_CR2 |= FLASH_CR_LOCK; } +#endif /* !WOLFBOOT_UNIT_TEST_FLASH_ERASE */ int RAMFUNCTION hal_flash_erase(uint32_t address, int len) { @@ -220,9 +224,14 @@ int RAMFUNCTION hal_flash_erase(uint32_t address, int len) (p <= (FLASH_TOP - FLASHMEM_ADDRESS_SPACE))) { uint32_t reg = FLASH_CR2 & (~((FLASH_CR_SNB_MASK << FLASH_CR_SNB_SHIFT) | FLASH_CR_PSIZE)); - p-= (FLASH_BANK2_BASE); + /* Sector index within bank 2: subtract the bank-2 base expressed in + * the same relative-offset convention as p (FLASH_BANK2_BASE_REL), + * not the absolute FLASH_BANK2_BASE which would underflow. Use a + * temporary so the loop variable p is not corrupted. */ + uint32_t sector = (p - FLASH_BANK2_BASE_REL) >> 17; FLASH_CR2 = reg | - (((p >> 17) << FLASH_CR_SNB_SHIFT) | FLASH_CR_SER | 0x00); + (((sector & FLASH_CR_SNB_MASK) << FLASH_CR_SNB_SHIFT) | + FLASH_CR_SER | 0x00); DMB(); FLASH_CR2 |= FLASH_CR_STRT; flash_wait_complete(FLASH_BANK_2); @@ -231,6 +240,7 @@ int RAMFUNCTION hal_flash_erase(uint32_t address, int len) return 0; } +#ifndef WOLFBOOT_UNIT_TEST_FLASH_ERASE #ifdef DEBUG_UART static int uart_init(void) { @@ -622,3 +632,4 @@ int hal_flash_otp_read(uint32_t flashAddress, void* data, uint32_t length) } #endif /* FLASH_OTP_KEYSTORE */ +#endif /* !WOLFBOOT_UNIT_TEST_FLASH_ERASE */ diff --git a/src/boot_arm.c b/src/boot_arm.c index 0a54e685a8..43efef9324 100644 --- a/src/boot_arm.c +++ b/src/boot_arm.c @@ -35,7 +35,9 @@ extern unsigned int _end_bss; extern uint32_t *END_STACK; +#ifndef WOLFBOOT_UNIT_TEST_MPU extern void main(void); +#endif #ifdef TARGET_va416x0 extern void SysTick_Handler(void); #endif @@ -94,7 +96,17 @@ static void mpu_on(void) #define MPUSIZE_16K (0x0d << 1) #define MPUSIZE_32K (0x0e << 1) #define MPUSIZE_64K (0x0f << 1) -/* ... */ +#define MPUSIZE_128K (0x10 << 1) +#define MPUSIZE_256K (0x11 << 1) +#define MPUSIZE_512K (0x12 << 1) +#define MPUSIZE_1M (0x13 << 1) +#define MPUSIZE_2M (0x14 << 1) +#define MPUSIZE_4M (0x15 << 1) +#define MPUSIZE_8M (0x16 << 1) +#define MPUSIZE_16M (0x17 << 1) +#define MPUSIZE_32M (0x18 << 1) +#define MPUSIZE_64M (0x19 << 1) +#define MPUSIZE_128M (0x1a << 1) #define MPUSIZE_256M (0x1b << 1) #define MPUSIZE_512M (0x1c << 1) #define MPUSIZE_1G (0x1d << 1) @@ -103,14 +115,17 @@ static void mpu_on(void) static uint32_t mpusize(uint32_t size) { - if (size <= (8 * 1024)) - return MPUSIZE_8K; - if (size <= (16 * 1024)) - return MPUSIZE_16K; - if (size <= (32 * 1024)) - return MPUSIZE_32K; - if (size <= (64 * 1024)) - return MPUSIZE_64K; + uint32_t rasr = MPUSIZE_8K; + uint32_t limit = (8 * 1024); + + while ((size > limit) && (limit < (128 * 1024 * 1024))) { + limit <<= 1; + rasr += 2; + } + + if (size <= limit) + return rasr; + return MPUSIZE_ERR; } @@ -175,6 +190,11 @@ static void RAMFUNCTION mpu_off(void) #define mpu_off() do{}while(0) #endif /* !WOLFBOOT_NO_MPU */ +#ifndef WOLFBOOT_UNIT_TEST_MPU +/* The remainder of this file is Cortex-M/-R reset/boot code that relies on + * ARM inline assembly and therefore cannot be compiled for the host. The MPU + * size helpers above are pure arithmetic and are exercised by the host unit + * test tools/unit-tests/unit-mpusize.c, which defines WOLFBOOT_UNIT_TEST_MPU. */ #ifdef CORTEX_R5 #define MINITGCR ((volatile uint32_t *)0xFFFFFF5C) @@ -710,3 +730,5 @@ void RAMFUNCTION arch_reboot(void) wolfBoot_panic(); } #endif /* RAM_CODE */ + +#endif /* !WOLFBOOT_UNIT_TEST_MPU */ diff --git a/src/flash_otp_keystore.c b/src/flash_otp_keystore.c index a66eb809d5..37e0a53ffd 100644 --- a/src/flash_otp_keystore.c +++ b/src/flash_otp_keystore.c @@ -24,9 +24,13 @@ #include #include +#ifndef WOLFBOOT_UNIT_TEST_OTP_KEYSTORE #include "wolfboot/wolfboot.h" +#endif #include "keystore.h" +#ifndef WOLFBOOT_UNIT_TEST_OTP_KEYSTORE #include "hal.h" +#endif #include "otp_keystore.h" #if defined(FLASH_OTP_KEYSTORE) && !defined(WOLFBOOT_NO_SIGN) @@ -70,6 +74,12 @@ int keystore_get_size(int id) SIZEOF_KEYSTORE_SLOT) != 0) return -1; slot = (struct keystore_slot *)otp_slot_item_cache; + /* The pubkey is cached in a fixed-size buffer holding at most + * KEYSTORE_PUBKEY_SIZE bytes. A larger pubkey_size read from a corrupted or + * mis-provisioned OTP slot would make callers read past the buffer, so + * reject it like other invalid OTP fields. */ + if (slot->pubkey_size > KEYSTORE_PUBKEY_SIZE) + return -1; return slot->pubkey_size; } diff --git a/src/pci.c b/src/pci.c index 83f5f83f48..6482b7a2c2 100644 --- a/src/pci.c +++ b/src/pci.c @@ -70,6 +70,14 @@ #define PCI_IO32_BASE 0x2000 #endif /* PCI_IO32_BASE */ +#ifndef PCI_IO32_LIMIT +/* x86 IO space is 16-bit: valid IO addresses are 0x0000-0xFFFF. IO BARs must + * not be allocated past this ceiling, otherwise the 8-bit bridge IO base/limit + * registers (which only carry address bits [15:8]) silently truncate the high + * bits, mis-programming the window onto the legacy IO range (PIC/PIT/RTC). */ +#define PCI_IO32_LIMIT 0x10000 +#endif /* PCI_IO32_LIMIT */ + #define PCI_ENUM_MAX_DEV 32 #define PCI_ENUM_MAX_FUN 8 #define PCI_ENUM_MAX_BARS 6 @@ -474,7 +482,7 @@ static int pci_program_bar(uint8_t bus, uint8_t dev, uint8_t fun, if ((bar_align & PCI_DATA_HI16_MASK) == 0) bar_align |= PCI_DATA_HI16_MASK; base = &info->io; - limit = 0xffffffff; + limit = PCI_IO32_LIMIT; } PCI_DEBUG_PRINTF("PCI enum: %s %x:%x.%x bar: %d val: %x (%s %s)\r\n", diff --git a/tools/keytools/sign.c b/tools/keytools/sign.c index bccf8fadd8..b23633a7b4 100644 --- a/tools/keytools/sign.c +++ b/tools/keytools/sign.c @@ -2508,6 +2508,38 @@ static int base_diff(const char *f_base, uint8_t *pubkey, uint32_t pubkey_sz, in } len3++; } + /* make_header_delta() below calls make_header_ex(is_diff=1), which may grow + * CMD.header_sz to fit the delta TLVs plus certificate chain. Resolve that + * expansion here, using the same logic, so patch_inv_off reflects the header + * size actually written; otherwise HDR_IMG_DELTA_INVERSE would encode a + * stale, too-small offset and break inverse-patch rollback. */ + if (CMD.cert_chain_file != NULL) { + struct stat cc_stat; + if ((stat(CMD.cert_chain_file, &cc_stat) == 0) && + (cc_stat.st_size >= 0)) { + if ((uintmax_t)cc_stat.st_size > (uintmax_t)UINT16_MAX) { + printf("Error: Certificate chain too large for TLV encoding " + "(%ju > %u)\n", (uintmax_t)cc_stat.st_size, UINT16_MAX); + goto cleanup; + } + else { + uint32_t required_space = header_required_size(1, + (uint32_t)cc_stat.st_size, 0); + if (CMD.header_sz < required_space) { + uint32_t new_size = 256; + while (new_size < required_space) { + if (new_size > (UINT32_MAX / 2U)) { + printf("Error: Header size overflow while sizing " + "certificate chain\n"); + goto cleanup; + } + new_size *= 2; + } + CMD.header_sz = new_size; + } + } + } + } patch_inv_off = (uint32_t)len3 + CMD.header_sz; patch_inv_sz = 0; diff --git a/tools/keytools/sign.py b/tools/keytools/sign.py index 6acd49aac7..75d57a100d 100755 --- a/tools/keytools/sign.py +++ b/tools/keytools/sign.py @@ -764,9 +764,9 @@ def make_header(image_file, fw_version, extra_fields=[]): base_version = re.split("_", (re.split("_v", delta_base_file)[1]))[0] header = make_header(tmp_outfile, fw_version, [[HDR_IMG_DELTA_BASE, 4, struct.pack(" UINT64_MAX - (p_memsz - 1)) { + fprintf(stderr, + "Skipping segment %zu (LMA 0x%lx, size 0x%lx) - " + "address range overflows 64-bit space\n", + i, (unsigned long)p_paddr, + (unsigned long)p_memsz); + continue; + } + else { + segmentEnd = p_paddr + p_memsz - 1; + } /* Check if segment start and end are both within any range */ bool startInRange = diff --git a/tools/squashelf/test-range-overflow.py b/tools/squashelf/test-range-overflow.py new file mode 100644 index 0000000000..45a6cf181c --- /dev/null +++ b/tools/squashelf/test-range-overflow.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +# test-range-overflow.py +# +# Regression test for the uint64_t overflow in squashelf's range filter +# (segmentEnd = p_paddr + p_memsz - 1). A crafted ELF64 PT_LOAD segment whose +# p_paddr + p_memsz wraps past 2^64 must NOT be smuggled past a range filter: +# its true span covers (almost) the whole address space, so it is out of range +# and must be excluded. Before the fix the wrapped end landed back inside the +# range and the segment was wrongly kept. +# +# 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. + +import os +import struct +import subprocess +import sys +import tempfile + +EHSIZE = 64 +PHSIZE = 56 +UINT64_MAX = 0xFFFFFFFFFFFFFFFF + + +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: + # 1) Overflow segment: p_paddr + p_memsz - 1 wraps below p_paddr, so the + # wrapped end (~0x4fe) is inside [0, 0x1000] even though the real span + # covers the whole address space. It MUST be excluded (non-zero exit, + # no output segment). + bad = os.path.join(d, "overflow.elf") + make_elf64(bad, paddr=0x500, memsz=UINT64_MAX) + if run(squashelf, bad, os.path.join(d, "bad.out"), "0x0-0x1000") == 0: + print("FAIL: overflow segment was wrongly included by range filter") + rc = 1 + else: + print("PASS: overflow segment excluded") + + # 2) Regression guard: a normal in-range segment must still be kept. + good = os.path.join(d, "ok.elf") + make_elf64(good, paddr=0x500, memsz=0x100) + if run(squashelf, good, os.path.join(d, "good.out"), "0x0-0x1000") != 0: + print("FAIL: normal in-range segment was dropped") + rc = 1 + else: + print("PASS: normal in-range segment kept") + + sys.exit(rc) + + +if __name__ == "__main__": + main() diff --git a/tools/test.mk b/tools/test.mk index e7e0a4d446..ed54b306b2 100644 --- a/tools/test.mk +++ b/tools/test.mk @@ -1185,52 +1185,52 @@ test-all: clean test-size-all: - make test-size SIGN=NONE LIMIT=5066 NO_ARM_ASM=1 + make test-size SIGN=NONE LIMIT=5048 NO_ARM_ASM=1 make keysclean - make test-size SIGN=ED25519 LIMIT=11852 NO_ARM_ASM=1 + make test-size SIGN=ED25519 LIMIT=11844 NO_ARM_ASM=1 make keysclean - make test-size SIGN=ECC256 LIMIT=18944 NO_ARM_ASM=1 + make test-size SIGN=ECC256 LIMIT=18880 NO_ARM_ASM=1 make clean - make test-size SIGN=ECC256 NO_ASM=1 LIMIT=13914 NO_ARM_ASM=1 + make test-size SIGN=ECC256 NO_ASM=1 LIMIT=13896 NO_ARM_ASM=1 make keysclean - make test-size SIGN=RSA2048 LIMIT=11916 NO_ARM_ASM=1 + make test-size SIGN=RSA2048 LIMIT=11768 NO_ARM_ASM=1 make clean - make test-size SIGN=RSA2048 NO_ASM=1 LIMIT=12496 NO_ARM_ASM=1 + make test-size SIGN=RSA2048 NO_ASM=1 LIMIT=12328 NO_ARM_ASM=1 make keysclean - make test-size SIGN=RSA4096 LIMIT=12204 NO_ARM_ASM=1 + make test-size SIGN=RSA4096 LIMIT=12068 NO_ARM_ASM=1 make clean - make test-size SIGN=RSA4096 NO_ASM=1 LIMIT=12784 NO_ARM_ASM=1 + make test-size SIGN=RSA4096 NO_ASM=1 LIMIT=12608 NO_ARM_ASM=1 make keysclean - make test-size SIGN=ECC384 LIMIT=19888 NO_ARM_ASM=1 + make test-size SIGN=ECC384 LIMIT=19564 NO_ARM_ASM=1 make clean - make test-size SIGN=ECC384 NO_ASM=1 LIMIT=15290 NO_ARM_ASM=1 + make test-size SIGN=ECC384 NO_ASM=1 LIMIT=15260 NO_ARM_ASM=1 make keysclean - make test-size SIGN=ED448 LIMIT=13952 NO_ARM_ASM=1 + make test-size SIGN=ED448 LIMIT=13928 NO_ARM_ASM=1 make keysclean - make test-size SIGN=RSA3072 LIMIT=12056 NO_ARM_ASM=1 + make test-size SIGN=RSA3072 LIMIT=11908 NO_ARM_ASM=1 make clean - make test-size SIGN=RSA3072 NO_ASM=1 LIMIT=12600 NO_ARM_ASM=1 + make test-size SIGN=RSA3072 NO_ASM=1 LIMIT=12436 NO_ARM_ASM=1 make keysclean - make test-size SIGN=RSAPSS2048 LIMIT=13680 NO_ARM_ASM=1 + make test-size SIGN=RSAPSS2048 LIMIT=13672 NO_ARM_ASM=1 make clean - make test-size SIGN=RSAPSS2048 NO_ASM=1 LIMIT=14240 NO_ARM_ASM=1 + make test-size SIGN=RSAPSS2048 NO_ASM=1 LIMIT=14232 NO_ARM_ASM=1 make keysclean - make test-size SIGN=RSAPSS3072 LIMIT=13848 NO_ARM_ASM=1 + make test-size SIGN=RSAPSS3072 LIMIT=13840 NO_ARM_ASM=1 make clean - make test-size SIGN=RSAPSS3072 NO_ASM=1 LIMIT=14372 NO_ARM_ASM=1 + make test-size SIGN=RSAPSS3072 NO_ASM=1 LIMIT=14364 NO_ARM_ASM=1 make keysclean - make test-size SIGN=RSAPSS4096 LIMIT=14020 NO_ARM_ASM=1 + make test-size SIGN=RSAPSS4096 LIMIT=14012 NO_ARM_ASM=1 make clean - make test-size SIGN=RSAPSS4096 NO_ASM=1 LIMIT=14560 NO_ARM_ASM=1 + make test-size SIGN=RSAPSS4096 NO_ASM=1 LIMIT=14552 NO_ARM_ASM=1 make keysclean make test-size SIGN=LMS LMS_LEVELS=2 LMS_HEIGHT=5 LMS_WINTERNITZ=8 \ WOLFBOOT_SMALL_STACK=0 IMAGE_SIGNATURE_SIZE=2644 \ - IMAGE_HEADER_SIZE?=5288 LIMIT=8084 NO_ARM_ASM=1 + IMAGE_HEADER_SIZE?=5288 LIMIT=8076 NO_ARM_ASM=1 make keysclean make test-size SIGN=XMSS XMSS_PARAMS='XMSS-SHA2_10_256' \ IMAGE_SIGNATURE_SIZE=2500 IMAGE_HEADER_SIZE?=4096 \ - LIMIT=8944 NO_ARM_ASM=1 + LIMIT=8728 NO_ARM_ASM=1 make keysclean make clean - make test-size SIGN=ML_DSA ML_DSA_LEVEL=2 LIMIT=19800 \ + make test-size SIGN=ML_DSA ML_DSA_LEVEL=2 LIMIT=19486 \ IMAGE_SIGNATURE_SIZE=2420 IMAGE_HEADER_SIZE?=8192 diff --git a/tools/tpm/rot.c b/tools/tpm/rot.c index 9c08d06752..9de7bce1e1 100644 --- a/tools/tpm/rot.c +++ b/tools/tpm/rot.c @@ -161,11 +161,20 @@ static int TPM2_Boot_SecureROT_Example(TPMI_RH_NV_AUTH authHandle, word32 nvBase } if (rc == 0) { digestSz = nvPublic.dataSize; + word32 digestReadSz; + /* dataSize is supplied by the TPM over the bus; clamp it to the + * digest buffer so a malicious/emulated TPM (or a pre-existing NV + * index larger than the hash) cannot overflow digest[] during the + * read-back below, which uses digestSz as the copy count. */ + if (digestSz > (int)sizeof(digest)) + digestSz = (int)sizeof(digest); + digestReadSz = (word32)digestSz; /* Read access */ printf("Reading NV 0x%x public key hash\n", nv.handle.hndl); rc = wolfTPM2_NVReadAuth(&dev, &nv, nv.handle.hndl, - digest, (word32*)&digestSz, 0); + digest, &digestReadSz, 0); + digestSz = (int)digestReadSz; } if (rc == 0) { printf("Read Public Key Hash (%d)\n", digestSz); diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index 0ce0815e4c..6bd15d38e0 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -61,6 +61,9 @@ TESTS+=unit-tpm-check-rot-auth TESTS+=unit-tpm-api-names TESTS+=unit-fit-gzip unit-fit-nogzip TESTS+=unit-fit-fpga +TESTS+=unit-mpusize +TESTS+=unit-flash-erase-h7 +TESTS+=unit-otp-keystore # linux_loader.c is x86 32-bit only, so its unit tests need a working 32-bit # (multilib) toolchain. Probe whether "gcc -m32" can link, and only add the @@ -96,6 +99,8 @@ run: $(TESTS) for unit in $(TESTS); do \ WOLFBOOT_SECTOR_SIZE=0x400 ./$$unit || exit 1; \ done + python3 unit-sign-delta-tlv.py || exit 1 + python3 unit-sign-delta-cert-inv-off.py || exit 1 WOLFCRYPT_SRC:=$(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha.c \ @@ -254,6 +259,22 @@ unit-string: ../../include/target.h unit-string.c unit-max-space: ../../include/target.h unit-max-space.c gcc -o $@ $^ $(CFLAGS) $(LDFLAGS) +# unit-mpusize includes src/boot_arm.c directly (guarded to its host-portable +# MPU helpers via WOLFBOOT_UNIT_TEST_MPU), so boot_arm.c is not a separate input. +unit-mpusize: ../../include/target.h unit-mpusize.c + gcc -o $@ unit-mpusize.c $(CFLAGS) $(LDFLAGS) + +# unit-flash-erase-h7 includes hal/stm32h7.c directly (guarded to hal_flash_erase +# via WOLFBOOT_UNIT_TEST_FLASH_ERASE), so stm32h7.c is not a separate input. +unit-flash-erase-h7: unit-flash-erase-h7.c ../../hal/stm32h7.c + gcc -o $@ unit-flash-erase-h7.c $(CFLAGS) $(LDFLAGS) + +# unit-otp-keystore includes src/flash_otp_keystore.c directly (guarded to its +# host-portable code via WOLFBOOT_UNIT_TEST_OTP_KEYSTORE), so it is not a +# separate input. +unit-otp-keystore: unit-otp-keystore.c ../../src/flash_otp_keystore.c + gcc -o $@ unit-otp-keystore.c $(CFLAGS) $(LDFLAGS) + unit-update-flash-self-update: ../../include/target.h unit-update-flash.c gcc -o $@ unit-update-flash.c ../../src/image.c \ $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha256.c \ diff --git a/tools/unit-tests/unit-flash-erase-h7.c b/tools/unit-tests/unit-flash-erase-h7.c new file mode 100644 index 0000000000..44e9a0b65d --- /dev/null +++ b/tools/unit-tests/unit-flash-erase-h7.c @@ -0,0 +1,164 @@ +/* unit-flash-erase-h7.c + * + * Unit tests for the dual-bank sector arithmetic in hal_flash_erase() + * (hal/stm32h7.c). + * + * + * 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 + +/* hal/stm32h7.c is tightly coupled to STM32H7 hardware registers and to a + * partition layout from target.h (its header even #errors on small partition + * sizes). Compile only hal_flash_erase() in isolation and provide host-side + * mocks for the few register accesses and constants it needs. The constant + * values mirror hal/stm32h7.h. */ +#define WOLFBOOT_UNIT_TEST_FLASH_ERASE + +/* Constants (mirror hal/stm32h7.h) */ +#define FLASHMEM_ADDRESS_SPACE (0x08000000UL) +#define FLASH_PAGE_SIZE (0x20000) /* 128KB */ +#define FLASH_BANK2_BASE (0x08100000UL) +#define FLASH_BANK2_BASE_REL (FLASH_BANK2_BASE - FLASHMEM_ADDRESS_SPACE) +#define FLASH_TOP (0x081FFFFFUL) +#define FLASH_CR_SNB_SHIFT 8 +#define FLASH_CR_SNB_MASK 0x7 +#define FLASH_CR_STRT (1 << 7) +#define FLASH_CR_PSIZE (1 << 4) +#define FLASH_CR_SER (1 << 2) +#define FLASH_BANK_1 0 +#define FLASH_BANK_2 1 + +#define RAMFUNCTION + +/* Mocked flash control registers */ +static uint32_t mock_FLASH_CR1; +static uint32_t mock_FLASH_CR2; +#define FLASH_CR1 mock_FLASH_CR1 +#define FLASH_CR2 mock_FLASH_CR2 + +/* Capture every programmed (CR1, CR2) pair. The driver issues a DMB() right + * after writing the sector number into FLASH_CRx and before setting STRT, so + * recording on DMB() snapshots each erase command exactly once per loop + * iteration. */ +#define ERASE_LOG_MAX 64 +static uint32_t erase_cr1[ERASE_LOG_MAX]; +static uint32_t erase_cr2[ERASE_LOG_MAX]; +static int erase_log_n; + +#define DMB() do { \ + if (erase_log_n < ERASE_LOG_MAX) { \ + erase_cr1[erase_log_n] = mock_FLASH_CR1; \ + erase_cr2[erase_log_n] = mock_FLASH_CR2; \ + erase_log_n++; \ + } \ +} while (0) + +/* hal_flash_erase() polls hardware via flash_wait_complete(); a no-op suffices + * on the host. */ +static void flash_wait_complete(uint8_t bank) { (void)bank; } + +static void reset_mocks(void) +{ + mock_FLASH_CR1 = 0; + mock_FLASH_CR2 = 0; + erase_log_n = 0; +} + +#include "../../hal/stm32h7.c" + +/* Decode the SNB sector field programmed into a captured CRx value. */ +static uint32_t snb_of(uint32_t cr) +{ + return (cr >> FLASH_CR_SNB_SHIFT) & FLASH_CR_SNB_MASK; +} + +/* Regression for F-5129: the Bank 2 branch subtracted the absolute + * FLASH_BANK2_BASE (0x08100000) from the relative offset p instead of + * FLASH_BANK2_BASE_REL (0x00100000). That underflowed uint32_t, so the wrong + * sector index was programmed into FLASH_CR2 and the loop variable was clobbered + * to ~0xF8000000, terminating any multi-sector Bank 2 erase after a single + * iteration. The canonical stm32h7 SWAP lives at 0x081C0000 (Bank 2 sector 6). */ +START_TEST(test_erase_bank2_two_sectors) +{ + reset_mocks(); + /* Erase Bank 2 sectors 6 and 7 (0x081C0000 .. 0x081FFFFF). */ + hal_flash_erase(0x081C0000UL, 2 * FLASH_PAGE_SIZE); + + /* Both sectors must be erased: two FLASH_CR2 commands, not one. */ + ck_assert_int_eq(erase_log_n, 2); + /* Each command must target the correct Bank 2 sector. */ + ck_assert_uint_eq(snb_of(erase_cr2[0]), 6); + ck_assert_uint_eq(snb_of(erase_cr2[1]), 7); +} +END_TEST + +/* A single-sector Bank 2 erase must program the actually requested sector, not + * sector 0 (the underflow always produced sector 0). */ +START_TEST(test_erase_bank2_single_sector) +{ + reset_mocks(); + hal_flash_erase(0x081C0000UL, FLASH_PAGE_SIZE); + + ck_assert_int_eq(erase_log_n, 1); + ck_assert_uint_eq(snb_of(erase_cr2[0]), 6); +} +END_TEST + +/* Bank 1 path is unaffected and must still walk every requested sector. */ +START_TEST(test_erase_bank1_two_sectors) +{ + reset_mocks(); + /* Bank 1 sectors 0 and 1 (0x08000000 .. 0x0803FFFF). */ + hal_flash_erase(0x08000000UL, 2 * FLASH_PAGE_SIZE); + + ck_assert_int_eq(erase_log_n, 2); + ck_assert_uint_eq(snb_of(erase_cr1[0]), 0); + ck_assert_uint_eq(snb_of(erase_cr1[1]), 1); +} +END_TEST + +Suite *flash_erase_suite(void) +{ + Suite *s = suite_create("flash-erase-h7"); + TCase *tc = tcase_create("flash-erase-h7"); + + tcase_add_test(tc, test_erase_bank2_two_sectors); + tcase_add_test(tc, test_erase_bank2_single_sector); + tcase_add_test(tc, test_erase_bank1_two_sectors); + + suite_add_tcase(s, tc); + return s; +} + +int main(void) +{ + int fails; + Suite *s = flash_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-mpusize.c b/tools/unit-tests/unit-mpusize.c new file mode 100644 index 0000000000..666056b53d --- /dev/null +++ b/tools/unit-tests/unit-mpusize.c @@ -0,0 +1,122 @@ +/* unit-mpusize.c + * + * Unit tests for the Cortex-M MPU region-size lookup in src/boot_arm.c. + * + * + * 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 + +/* Compile only the host-portable MPU helpers from boot_arm.c. The rest of the + * file is ARM reset/boot code built around inline assembly. */ +#define WOLFBOOT_UNIT_TEST_MPU + +/* Linker symbols referenced by mpu_init(); never dereferenced by the tests. */ +unsigned int _start_text; +unsigned int _stored_data; +unsigned int _start_data; +unsigned int _end_data; +unsigned int _start_bss; +unsigned int _end_bss; +uint32_t *END_STACK; + +#include "../../src/boot_arm.c" + +/* The MPU SIZE field encodes region size as 2^(field+1); mpusize() rounds the + * requested size up to the next supported power of two and shifts the field + * left by one to land in the RASR layout. */ +static uint32_t expected_rasr(uint32_t field) +{ + return field << 1; +} + +START_TEST(test_mpusize_small_sizes) +{ + ck_assert_uint_eq(mpusize(1), MPUSIZE_8K); + ck_assert_uint_eq(mpusize(8 * 1024), MPUSIZE_8K); + ck_assert_uint_eq(mpusize(8 * 1024 + 1), MPUSIZE_16K); + ck_assert_uint_eq(mpusize(32 * 1024), MPUSIZE_32K); + ck_assert_uint_eq(mpusize(64 * 1024), MPUSIZE_64K); +} +END_TEST + +/* Regression for F-5132: builds whose .text+.rodata exceed 64 KB (TrustZone, + * PQC, delta-update, multi-algorithm) must still map to a valid MPU region size + * rather than MPUSIZE_ERR, which would make mpu_init() bail before mpu_on() and + * silently disable the MPU. */ +START_TEST(test_mpusize_above_64k_is_not_err) +{ + ck_assert_uint_ne(mpusize(64 * 1024 + 1), MPUSIZE_ERR); + ck_assert_uint_ne(mpusize(80 * 1024), MPUSIZE_ERR); + ck_assert_uint_ne(mpusize(128 * 1024), MPUSIZE_ERR); + ck_assert_uint_ne(mpusize(256 * 1024), MPUSIZE_ERR); + ck_assert_uint_ne(mpusize(512 * 1024), MPUSIZE_ERR); + ck_assert_uint_ne(mpusize(1024 * 1024), MPUSIZE_ERR); +} +END_TEST + +START_TEST(test_mpusize_large_sizes_round_up) +{ + ck_assert_uint_eq(mpusize(64 * 1024 + 1), MPUSIZE_128K); + ck_assert_uint_eq(mpusize(128 * 1024), MPUSIZE_128K); + ck_assert_uint_eq(mpusize(128 * 1024 + 1), MPUSIZE_256K); + ck_assert_uint_eq(mpusize(512 * 1024), MPUSIZE_512K); + ck_assert_uint_eq(mpusize(1024 * 1024), MPUSIZE_1M); + ck_assert_uint_eq(mpusize(128 * 1024 * 1024), MPUSIZE_128M); +} +END_TEST + +/* Every returned value must carry the SIZE field in the RASR bit layout + * (field << 1), and decode back to a power-of-two region >= the request. */ +START_TEST(test_mpusize_encoding) +{ + ck_assert_uint_eq(mpusize(128 * 1024), expected_rasr(0x10)); + ck_assert_uint_eq(mpusize(128 * 1024 * 1024), expected_rasr(0x1a)); +} +END_TEST + +Suite *mpusize_suite(void) +{ + Suite *s = suite_create("mpusize"); + TCase *tc = tcase_create("mpusize"); + + tcase_add_test(tc, test_mpusize_small_sizes); + tcase_add_test(tc, test_mpusize_above_64k_is_not_err); + tcase_add_test(tc, test_mpusize_large_sizes_round_up); + tcase_add_test(tc, test_mpusize_encoding); + + suite_add_tcase(s, tc); + return s; +} + +int main(void) +{ + int fails; + Suite *s = mpusize_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-otp-keystore.c b/tools/unit-tests/unit-otp-keystore.c new file mode 100644 index 0000000000..609a1fb7c8 --- /dev/null +++ b/tools/unit-tests/unit-otp-keystore.c @@ -0,0 +1,126 @@ +/* unit-otp-keystore.c + * + * Unit tests for keystore_get_size() in src/flash_otp_keystore.c. + * + * + * 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 + +/* flash_otp_keystore.c normally pulls in wolfboot/wolfboot.h and hal.h, which + * drag in target/HAL specifics. Compile it in isolation (guarded by + * WOLFBOOT_UNIT_TEST_OTP_KEYSTORE) and provide the few constants and the OTP + * read mock it needs. */ +#define WOLFBOOT_UNIT_TEST_OTP_KEYSTORE +#define FLASH_OTP_KEYSTORE +#define KEYSTORE_PUBKEY_SIZE 64 /* model an ECC-256 keystore slot */ +#define OTP_SIZE 1024 +#define FLASH_OTP_BASE 0 + +/* Mock OTP flash backing store and reader. With FLASH_OTP_BASE == 0 the address + * passed by the driver is a plain offset into this buffer. */ +static uint8_t mock_otp[OTP_SIZE]; + +int hal_flash_otp_read(uint32_t flashAddress, void *data, uint32_t length) +{ + if (flashAddress + length > (uint32_t)OTP_SIZE) + return -1; + memcpy(data, mock_otp + flashAddress, length); + return 0; +} + +#include "../../src/flash_otp_keystore.c" + +/* Provision the mock OTP with a single keystore slot whose pubkey_size field is + * set to the supplied (possibly bogus) value. */ +static void setup_otp(uint32_t pubkey_size) +{ + struct wolfBoot_otp_hdr *hdr = (struct wolfBoot_otp_hdr *)mock_otp; + struct keystore_slot *slot = + (struct keystore_slot *)(mock_otp + OTP_HDR_SIZE); + + memset(mock_otp, 0, sizeof(mock_otp)); + memcpy(hdr->keystore_hdr_magic, KEYSTORE_HDR_MAGIC, 8); + hdr->item_count = 1; + slot->slot_id = 0; + slot->key_type = 1; + slot->part_id_mask = 0xFFFFFFFF; + slot->pubkey_size = pubkey_size; +} + +/* A correctly provisioned slot returns its real size unchanged. */ +START_TEST(test_valid_size_passthrough) +{ + setup_otp(KEYSTORE_PUBKEY_SIZE); + ck_assert_int_eq(keystore_get_size(0), KEYSTORE_PUBKEY_SIZE); +} +END_TEST + +/* Regression for F-4790: keystore_get_size() must never report a size larger + * than the KEYSTORE_PUBKEY_SIZE-byte buffer the pubkey is cached in. Without the + * clamp it returned the raw OTP value (here 2*KEYSTORE_PUBKEY_SIZE), which the + * callers (key_sha256/key_sha384/key_sha3_384 and the ECC verify path) used as a + * hash length / coordinate offset, reading past otp_slot_item_cache. */ +START_TEST(test_oversize_rejected) +{ + int sz; + setup_otp(2 * KEYSTORE_PUBKEY_SIZE); + sz = keystore_get_size(0); + ck_assert_int_le(sz, KEYSTORE_PUBKEY_SIZE); + ck_assert_int_eq(sz, -1); +} +END_TEST + +/* One byte over the buffer is still out of bounds and must be rejected. */ +START_TEST(test_just_over_rejected) +{ + setup_otp(KEYSTORE_PUBKEY_SIZE + 1); + ck_assert_int_eq(keystore_get_size(0), -1); +} +END_TEST + +Suite *otp_keystore_suite(void) +{ + Suite *s = suite_create("otp-keystore"); + TCase *tc = tcase_create("otp-keystore"); + + tcase_add_test(tc, test_valid_size_passthrough); + tcase_add_test(tc, test_oversize_rejected); + tcase_add_test(tc, test_just_over_rejected); + + suite_add_tcase(s, tc); + return s; +} + +int main(void) +{ + int fails; + Suite *s = otp_keystore_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-pci.c b/tools/unit-tests/unit-pci.c index bf1196aeee..1ff4021289 100644 --- a/tools/unit-tests/unit-pci.c +++ b/tools/unit-tests/unit-pci.c @@ -1243,6 +1243,55 @@ START_TEST(test_program_bridge) } END_TEST +/* test_program_bridge_io_64k_no_narrow: a device advertising a 64KB IO BAR + * must not push the IO allocator past the 16-bit IO space (0xFFFF). The PCI + * bridge IO base/limit registers carry only address bits [15:8] in a uint8_t, + * so an io cursor >= 0x10000 truncates: io_start 0x20000 -> base reg 0x00, + * programming a bogus window 0x0000-0x0FFF that forwards legacy IO (8259A PIC, + * 8254 PIT, MC146818 RTC) to the secondary bus. With the allocator capped at + * the 16-bit IO ceiling the oversized BAR is skipped and the bridge window is + * programmed from the real cursor. */ +START_TEST(test_program_bridge_io_64k_no_narrow) +{ + struct test_pci_topology t; + struct pci_enum_info info; + int dev0, br, ep; + uint8_t iobase, iolimit; + + test_pci_init(&t); + /* dev 0 on bus 0: hostile/oversized 64KB IO BAR (decodes only 16 bits) */ + dev0 = test_pci_add_dev(&t, 0, 0, 0x1111, 0x2222, TEST_PCI_ROOT_BUS); + test_pci_dev_set_bar(&t, dev0, 0, 0x10000, TEST_PCI_BAR_IO); + t.nodes[dev0].bars[0].io_hi16_zero = 1; + /* dev 1 on bus 0: bridge with a small 256B IO device behind it */ + br = test_pci_add_bridge(&t, 1, 0, 0xAAAA, 0xBBBB, TEST_PCI_ROOT_BUS); + ep = test_pci_add_dev(&t, 0, 0, 0xCCCC, 0xDDDD, br); + test_pci_dev_set_bar(&t, ep, 0, 256, TEST_PCI_BAR_IO); + test_pci_commit(&t); + + memset(&info, 0, sizeof(info)); + info.mem = 0x80000000; + info.mem_limit = 0x88000000; + info.mem_pf = 0x90000000; + info.mem_pf_limit = 0xFFFFFFFF; + info.io = 0x2000; + info.curr_bus_number = 0; + + pci_enum_bus(0, &info); + + /* The oversized IO BAR must be skipped, leaving the cursor in 16-bit IO + * space; the bridge IO window must reflect the real device (0x2000-0x2FFF) + * and never decode down to 0x0000 over the legacy IO range. */ + iobase = pci_config_read8(0, 1, 0, PCI_IO_BASE_OFF); + iolimit = pci_config_read8(0, 1, 0, PCI_IO_LIMIT_OFF); + ck_assert_uint_eq(iobase, 0x20); + ck_assert_uint_eq(iolimit, 0x2F); + ck_assert_uint_le(info.io, 0x10000); + + test_pci_cleanup(&t); +} +END_TEST + /* test_program_bridge_oom_initial: initial alignment failures */ START_TEST(test_program_bridge_oom_initial) { @@ -1335,13 +1384,11 @@ START_TEST(test_program_bridge_oom_post_enum) .mem_pf = 0x90000000, .mem_pf_limit = 0xFFFFFFFF, .io = 0x2000 } }, - { - "io: post-enum 4KB align wraps 32-bit space", - 256, TEST_PCI_BAR_IO, - { .mem = 0x80000000, .mem_limit = 0x88000000, - .mem_pf = 0x90000000, .mem_pf_limit = 0xFFFFFFFF, - .io = 0xFFFFF000 } - }, + /* No "io: post-enum align wraps" case: the IO allocator is capped at + * the 16-bit IO ceiling (PCI_IO32_LIMIT), so info.io can never reach + * the top of the 32-bit range and the post-enum IO alignment cannot + * wrap. Oversized IO BARs are now skipped at allocation time, covered + * by test_program_bridge_io_64k_no_narrow. */ }; int i; @@ -1751,6 +1798,10 @@ Suite *wolfboot_suite(void) tcase_add_test(tc_bridge, test_program_bridge); suite_add_tcase(s, tc_bridge); + TCase *tc_io_64k = tcase_create("bridge-io-64k-no-narrow"); + tcase_add_test(tc_io_64k, test_program_bridge_io_64k_no_narrow); + suite_add_tcase(s, tc_io_64k); + TCase *tc_oom_init = tcase_create("bridge-oom-initial"); tcase_add_test(tc_oom_init, test_program_bridge_oom_initial); suite_add_tcase(s, tc_oom_init); diff --git a/tools/unit-tests/unit-rot-auth.c b/tools/unit-tests/unit-rot-auth.c index 52a2855d3d..977b207f72 100644 --- a/tools/unit-tests/unit-rot-auth.c +++ b/tools/unit-tests/unit-rot-auth.c @@ -32,6 +32,8 @@ static uint8_t test_pubkey[32]; static int symmetric_corrupted; +static uint32_t mock_nv_datasize = 32; +static uint32_t mock_nvread_reqsz; #define TPM2_IoCb NULL #define XSTRTOL strtol @@ -86,7 +88,7 @@ int wolfTPM2_NVReadPublic(WOLFTPM2_DEV* dev, TPM_HANDLE nvIndex, (void)dev; (void)nvIndex; memset(nvPublic, 0, sizeof(*nvPublic)); - nvPublic->dataSize = 32; + nvPublic->dataSize = (UINT16)mock_nv_datasize; return 0; } @@ -102,7 +104,14 @@ int wolfTPM2_NVReadAuth(WOLFTPM2_DEV* dev, WOLFTPM2_NV* nv, TPM_HANDLE nvIndex, memset(&zero_sym, 0, sizeof(zero_sym)); symmetric_corrupted = memcmp(&nv->handle.symmetric, &zero_sym, sizeof(zero_sym)) != 0; - memset(dataBuf, 0xA5, *dataSz); + /* Record the byte count the caller asked us to copy. The real + * wolfTPM2_NVReadAuth uses this as the XMEMCPY count into dataBuf with no + * separate capacity argument, so the caller must clamp it to its buffer. + * Cap the actual write at WC_MAX_DIGEST_SIZE so this harness never itself + * overflows the caller's digest[] buffer regardless of the requested size. */ + mock_nvread_reqsz = *dataSz; + memset(dataBuf, 0xA5, + *dataSz > WC_MAX_DIGEST_SIZE ? WC_MAX_DIGEST_SIZE : *dataSz); return 0; } @@ -234,6 +243,31 @@ START_TEST(test_rot_rejects_oversized_auth) } END_TEST +START_TEST(test_rot_clamps_nv_datasize) +{ + char auth[] = "test-auth"; + int rc; + + /* Emulate a malicious/emulated TPM (or a pre-existing NV index) whose + * reported dataSize exceeds the 64-byte digest[] read buffer. The value + * must be clamped before being used as the read byte count, otherwise + * wolfTPM2_NVReadAuth overflows digest[WC_MAX_DIGEST_SIZE]. */ + memset(test_pubkey, 0x11, sizeof(test_pubkey)); + symmetric_corrupted = 0; + mock_nvread_reqsz = 0; + mock_nv_datasize = 1000; + + rc = TPM2_Boot_SecureROT_Example(TPM_RH_PLATFORM, + WOLFBOOT_TPM_KEYSTORE_NV_BASE, WC_HASH_TYPE_SHA256, 0, 0, auth, + (int)strlen(auth)); + + mock_nv_datasize = 32; /* restore default for other tests */ + + ck_assert_int_eq(rc, 0); + ck_assert_int_le(mock_nvread_reqsz, (uint32_t)WC_MAX_DIGEST_SIZE); +} +END_TEST + static Suite* rot_auth_suite(void) { Suite* s; @@ -242,6 +276,7 @@ static Suite* rot_auth_suite(void) s = suite_create("rot_auth"); tc = tcase_create("auth_validation"); tcase_add_test(tc, test_rot_rejects_oversized_auth); + tcase_add_test(tc, test_rot_clamps_nv_datasize); suite_add_tcase(s, tc); return s; } diff --git a/tools/unit-tests/unit-sign-delta-cert-inv-off.py b/tools/unit-tests/unit-sign-delta-cert-inv-off.py new file mode 100644 index 0000000000..314ce8482e --- /dev/null +++ b/tools/unit-tests/unit-sign-delta-cert-inv-off.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +# unit-sign-delta-cert-inv-off.py +# +# Regression test for the C signing tool delta inverse-patch offset. +# +# base_diff() in tools/keytools/sign.c captures +# patch_inv_off = len3 + CMD.header_sz +# BEFORE calling make_header_delta(), which signs the delta image with +# make_header_ex(is_diff=1). When a certificate chain is present the delta +# header needs ~72 extra bytes (four delta TLVs plus the base-hash TLV), so for +# a window of cert-chain sizes header_required_size(is_diff=0) still fits the +# current CMD.header_sz while header_required_size(is_diff=1) does not. In that +# window make_header_ex(is_diff=1) grows CMD.header_sz to the next power of two +# AFTER patch_inv_off was captured, so the HDR_IMG_DELTA_INVERSE TLV stored in +# the signed delta image encodes a stale, too-small offset. The bootloader +# (src/update_flash.c) uses that value as a raw byte offset into the update +# partition to locate the inverse patch for rollback, so a wrong value makes +# rollback read garbage and fail. +# +# This test signs a real delta image with the C sign tool using an ed25519 key +# and a 300-byte certificate chain (a value inside the triggering window for +# ed25519+sha256, where CMD.header_sz starts at 512 but the delta header needs +# 1024). It then checks the self-consistency invariant: the inverse patch is the +# trailing HDR_IMG_DELTA_INVERSE_SIZE bytes of the file, so +# HDR_IMG_DELTA_INVERSE == filesize - HDR_IMG_DELTA_INVERSE_SIZE +# must hold. Before the fix the stored offset is one power-of-two short and this +# assertion fails. +# +# 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. + +import os +import struct +import subprocess +import sys +import tempfile + +# Tags from include/wolfboot/wolfboot.h +HDR_IMG_DELTA_INVERSE = 0x15 +HDR_IMG_DELTA_INVERSE_SIZE = 0x16 +HDR_PADDING = 0xFF + +SECTOR_SIZE = 0x1000 +# 300-byte chain: inside the ed25519+sha256 window where the is_diff=0 header +# fits 512 bytes but the is_diff=1 (delta) header needs 1024. +CERT_CHAIN_SZ = 300 + +THIS_DIR = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.abspath(os.path.join(THIS_DIR, "..", "..")) +SIGN = os.path.join(ROOT, "tools", "keytools", "sign") + + +def skip(msg): + print("SKIP unit-sign-delta-cert-inv-off: " + msg) + sys.exit(0) + + +def find_tlv_u32(data, want_type, scan_end): + """Mirror of wolfBoot_find_header(): return the u32 value for want_type.""" + p = 8 # skip 4-byte magic + 4-byte image size + while p + 4 <= scan_end: + htype = data[p] | (data[p + 1] << 8) + if htype == 0: + break + if data[p] == HDR_PADDING or (p & 1) != 0: + p += 1 + continue + length = data[p + 2] | (data[p + 3] << 8) + if htype == want_type: + if length != 4 or p + 4 + length > scan_end: + return None + return struct.unpack(" end: + break + if htype == want_type: + return length + p += 4 + length + return None + + +def ensure_bmdiff(): + if os.path.exists(BMDIFF): + return True + delta_dir = os.path.join(ROOT, "tools", "delta") + try: + subprocess.run( + ["gcc", "-o", "delta.o", "-c", os.path.join(ROOT, "src", "delta.c"), + "-I" + os.path.join(ROOT, "include"), "-DDELTA_UPDATES", + "-DWOLFBOOT_SECTOR_SIZE=0x%x" % SECTOR_SIZE], + cwd=delta_dir, check=True) + subprocess.run( + ["gcc", "-o", "bmdiff.o", "-c", "bmdiff.c", + "-I" + os.path.join(ROOT, "include"), "-DDELTA_UPDATES", + "-DWOLFBOOT_SECTOR_SIZE=0x%x" % SECTOR_SIZE], + cwd=delta_dir, check=True) + subprocess.run(["gcc", "-o", "bmdiff", "delta.o", "bmdiff.o"], + cwd=delta_dir, check=True) + except (subprocess.CalledProcessError, OSError): + return False + return os.path.exists(BMDIFF) + + +def main(): + try: + import wolfcrypt # noqa: F401 + except Exception: + skip("python wolfcrypt module not available") + if not os.path.exists(SIGN_PY): + skip("sign.py not found") + if not ensure_bmdiff(): + skip("could not build tools/delta/bmdiff") + + with tempfile.TemporaryDirectory() as work: + key = os.path.join(work, "priv.der") + with open(key, "wb") as f: + f.write(b"\x42" * 32) # 32-byte raw ed25519 private seed + + base = os.path.join(work, "image_v1.bin") + upd = os.path.join(work, "image_v2.bin") + payload = bytes((i * 7) & 0xFF for i in range(2048)) + with open(base, "wb") as f: + f.write(payload) + with open(upd, "wb") as f: + # small localized change so the patch stays well under 64 KB + f.write(payload[:512] + b"PATCHED!" + payload[520:]) + + env = dict(os.environ) + env["WOLFBOOT_SECTOR_SIZE"] = str(SECTOR_SIZE) + + # Sign the base image (v1) so it can be used as the delta base. + r = subprocess.run( + [sys.executable, SIGN_PY, "--ed25519", "--sha256", base, key, "1"], + cwd=ROOT, env=env, capture_output=True, text=True) + if r.returncode != 0: + skip("sign.py base sign failed: " + r.stderr.strip()) + signed_base = base.replace(".bin", "_v1_signed.bin") + if not os.path.exists(signed_base): + skip("sign.py did not produce a signed base image") + + # Sign the delta image (v2) against the signed base. + r = subprocess.run( + [sys.executable, SIGN_PY, "--ed25519", "--sha256", "--delta", + signed_base, upd, key, "2"], + cwd=ROOT, env=env, capture_output=True, text=True) + if r.returncode != 0: + skip("sign.py delta sign failed: " + r.stderr.strip()) + diff = upd.replace(".bin", "_v2_signed_diff.bin") + if not os.path.exists(diff): + skip("sign.py did not produce a delta image") + + with open(diff, "rb") as f: + header = f.read(IMAGE_HEADER_SIZE) + + failures = [] + for name, tag in (("HDR_IMG_DELTA_SIZE", HDR_IMG_DELTA_SIZE), + ("HDR_IMG_DELTA_INVERSE_SIZE", HDR_IMG_DELTA_INVERSE_SIZE)): + length = find_tlv(header, tag) + if length is None: + failures.append("%s TLV not found in delta header" % name) + elif length != SIZEOF_UINT32: + failures.append( + "%s encoded with length %d, but wolfBoot_get_delta_info() " + "requires sizeof(uint32_t)=%d; bootloader will reject the " + "image" % (name, length, SIZEOF_UINT32)) + + if failures: + for msg in failures: + print("FAIL unit-sign-delta-tlv: " + msg) + sys.exit(1) + + print("unit-sign-delta-tlv: OK") + sys.exit(0) + + +if __name__ == "__main__": + main()