From 3bfd7de34d02a810aafbee0e338f03bc5b8d57db Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 09:08:03 +0200 Subject: [PATCH 01/12] F-5674: cap PCI IO BAR allocator at the 16-bit IO ceiling A device advertising a 64KB IO BAR pushed the IO allocator cursor (info->io) past 0x10000 because pci_program_bar used 0xffffffff as the IO BAR limit. The PCI-to-PCI bridge IO base/limit registers are 8-bit and only carry address bits [15:8], so io_start >> 8 silently narrows a 0x20000 cursor to 0x00, programming a bogus bridge IO window 0x0000-0x0FFF that forwards legacy IO (8259A PIC, 8254 PIT, MC146818 RTC) to the secondary bus. x86 IO space is 16-bit, so IO BARs must never be allocated above 0xFFFF. Cap the IO BAR allocator limit at PCI_IO32_LIMIT (0x10000): oversized IO BARs are now skipped at allocation time and the bridge IO window is programmed from the real cursor, never narrowing onto legacy IO. Add test_program_bridge_io_64k_no_narrow proving the bridge IO window is no longer mis-programmed. The post-enum IO OOM case is removed as the cap makes that wrap unreachable. --- src/pci.c | 10 +++++- tools/unit-tests/unit-pci.c | 65 +++++++++++++++++++++++++++++++++---- 2 files changed, 67 insertions(+), 8 deletions(-) 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/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); From d280262028c6f81e8d3bdeab0bce30ba5eafde81 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 09:19:02 +0200 Subject: [PATCH 02/12] F-5356: bound i.MX RT flash write copy to caller buffer length hal_flash_write() programs one full CONFIG_FLASH_PAGE_SIZE (256/512 byte) page per loop iteration but unconditionally memcpy'd a whole page out of the caller's buffer regardless of len. Sub-page writes - notably the 1-byte trailer updates from set_trailer_at()/trailer_write() on the non-NVM_FLASH_WRITEONCE i.MX RT configs - therefore overread the source buffer (e.g. 255 bytes past a 1-byte stack value) and could program adjacent stack/RAM contents into flash. Bound the copy to min(page, len - i) and pad the rest of the page buffer with the erased value (0xFF). 0xFF is a no-op for NOR programming, so the existing flash contents of the rest of the page are preserved. --- hal/imx_rt.c | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) 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, From 84d5bd3ddedfb4020dbe7834c101e3c4de8f4503 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 09:33:24 +0200 Subject: [PATCH 03/12] F-5352: emit 4-byte delta size TLVs from Python signer sign.py encoded HDR_IMG_DELTA_SIZE and HDR_IMG_DELTA_INVERSE_SIZE with a 2-byte length via struct.pack(" 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() From 8d235cf21594b2de5c2a00b7c1613a9caa58b88a Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 09:43:07 +0200 Subject: [PATCH 04/12] F-5132: complete mpusize() table so MPU stays enabled for >64KB wolfBoot The mpusize() lookup in boot_arm.c only covered sizes up to 64KB and returned MPUSIZE_ERR for anything larger. mpu_init() passes the wolfBoot .text+.rodata span (_stored_data - _start_text) to mpusize() and bails out at the MPUSIZE_ERR guard before reaching mpu_on(). Any build whose bootloader image exceeds 64KB (TrustZone, PQC, delta-update, or several crypto algorithms) therefore left MPU_CTRL clear, silently disabling all five MPU regions for the lifetime of the bootloader. Fill in the missing power-of-two entries from 128KB through 128MB (ARMv7-M SIZE field = log2(bytes)-1, shifted into the RASR layout) so the flash region size is resolved and mpu_on() is reached. Add tools/unit-tests/unit-mpusize.c, which includes the real mpusize() from boot_arm.c (guarded to its host-portable MPU helpers via WOLFBOOT_UNIT_TEST_MPU) and checks that sizes above 64KB no longer map to MPUSIZE_ERR. The test fails before this fix and passes after. --- src/boot_arm.c | 43 ++++++++++- tools/unit-tests/Makefile | 6 ++ tools/unit-tests/unit-mpusize.c | 122 ++++++++++++++++++++++++++++++++ 3 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 tools/unit-tests/unit-mpusize.c diff --git a/src/boot_arm.c b/src/boot_arm.c index 0a54e685a8..5255492690 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) @@ -111,6 +123,28 @@ static uint32_t mpusize(uint32_t size) return MPUSIZE_32K; if (size <= (64 * 1024)) return MPUSIZE_64K; + if (size <= (128 * 1024)) + return MPUSIZE_128K; + if (size <= (256 * 1024)) + return MPUSIZE_256K; + if (size <= (512 * 1024)) + return MPUSIZE_512K; + if (size <= (1024 * 1024)) + return MPUSIZE_1M; + if (size <= (2 * 1024 * 1024)) + return MPUSIZE_2M; + if (size <= (4 * 1024 * 1024)) + return MPUSIZE_4M; + if (size <= (8 * 1024 * 1024)) + return MPUSIZE_8M; + if (size <= (16 * 1024 * 1024)) + return MPUSIZE_16M; + if (size <= (32 * 1024 * 1024)) + return MPUSIZE_32M; + if (size <= (64 * 1024 * 1024)) + return MPUSIZE_64M; + if (size <= (128 * 1024 * 1024)) + return MPUSIZE_128M; return MPUSIZE_ERR; } @@ -175,6 +209,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 +749,5 @@ void RAMFUNCTION arch_reboot(void) wolfBoot_panic(); } #endif /* RAM_CODE */ + +#endif /* !WOLFBOOT_UNIT_TEST_MPU */ diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index 91dcc3e685..ace181b3a2 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -61,6 +61,7 @@ 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 # 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 @@ -255,6 +256,11 @@ 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-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-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; +} From 74b8fc06647fc4439347616d9f007d3d7a76913a Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 09:52:28 +0200 Subject: [PATCH 05/12] F-5131: fix stale delta inverse-patch offset when cert chain expands header base_diff() captured patch_inv_off = len3 + CMD.header_sz before calling make_header_delta(), which signs the delta image via make_header_ex(is_diff=1). When a certificate chain is present, the delta (is_diff=1) header needs ~72 more bytes than the non-delta header for the four delta TLVs plus the base-hash TLV. For a window of cert-chain sizes, header_required_size(is_diff=0) still fit the current CMD.header_sz while header_required_size(is_diff=1) did not, so make_header_ex(is_diff=1) grew CMD.header_sz to the next power of two *after* patch_inv_off was captured. The HDR_IMG_DELTA_INVERSE TLV then encoded a stale, too-small offset; the bootloader (update_flash.c) uses it as a raw byte offset into the update partition to locate the inverse patch, so rollback read from the wrong offset and failed. Resolve the is_diff=1 header-size expansion (same logic as make_header_ex) before computing patch_inv_off. Add unit-sign-delta-cert-inv-off.py, which signs an ed25519 delta with a 300-byte chain (inside the triggering window) and asserts the inverse patch is the trailing HDR_IMG_DELTA_INVERSE_SIZE bytes of the file; it fails before this fix. --- tools/keytools/sign.c | 21 ++ tools/unit-tests/Makefile | 1 + .../unit-sign-delta-cert-inv-off.py | 186 ++++++++++++++++++ 3 files changed, 208 insertions(+) create mode 100644 tools/unit-tests/unit-sign-delta-cert-inv-off.py diff --git a/tools/keytools/sign.c b/tools/keytools/sign.c index bccf8fadd8..7370af467c 100644 --- a/tools/keytools/sign.c +++ b/tools/keytools/sign.c @@ -2508,6 +2508,27 @@ 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) && + ((uintmax_t)cc_stat.st_size <= (uintmax_t)UINT32_MAX)) { + 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) { + 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/unit-tests/Makefile b/tools/unit-tests/Makefile index ace181b3a2..d21dd6c111 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -98,6 +98,7 @@ run: $(TESTS) 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 \ 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..5f09fd5ef1 --- /dev/null +++ b/tools/unit-tests/unit-sign-delta-cert-inv-off.py @@ -0,0 +1,186 @@ +#!/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: + return struct.unpack(" Date: Tue, 9 Jun 2026 10:06:22 +0200 Subject: [PATCH 06/12] F-5129: fix stm32h7 hal_flash_erase Bank 2 sector underflow The Bank 2 branch of hal_flash_erase() subtracted the absolute base FLASH_BANK2_BASE (0x08100000) from the bank-relative loop offset p, instead of the relative FLASH_BANK2_BASE_REL (0x00100000) used by every other comparison in the function. For a Bank 2 offset (p >= 0x00100000) this underflowed uint32_t to ~0xF8000000, with two effects: 1. the SNB sector index programmed into FLASH_CR2 came from the underflowed value (always sector 0, with stray high bits leaking into other CR2 fields), and 2. the subtraction mutated the loop variable p itself, so after p += FLASH_PAGE_SIZE the offset jumped past any end_address and the loop exited after a single iteration. The combined result: any multi-sector Bank 2 erase touched only one sector with the wrong index, silently leaving the remaining requested sectors (e.g. the SWAP partition at 0x081C0000) unerased. Compute the sector index in a temporary from FLASH_BANK2_BASE_REL so the loop variable is preserved, and mask it with FLASH_CR_SNB_MASK before shifting into FLASH_CR2. Adds unit-flash-erase-h7, which compiles hal_flash_erase() in isolation (guarded by WOLFBOOT_UNIT_TEST_FLASH_ERASE, mirroring the unit-mpusize approach for boot_arm.c) against mocked flash registers and asserts that a two-sector Bank 2 erase programs sectors 6 and 7 across two iterations. --- hal/stm32h7.c | 15 ++- tools/unit-tests/Makefile | 6 + tools/unit-tests/unit-flash-erase-h7.c | 164 +++++++++++++++++++++++++ 3 files changed, 183 insertions(+), 2 deletions(-) create mode 100644 tools/unit-tests/unit-flash-erase-h7.c 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/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index d21dd6c111..a08180b9b9 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -62,6 +62,7 @@ TESTS+=unit-tpm-api-names TESTS+=unit-fit-gzip unit-fit-nogzip TESTS+=unit-fit-fpga TESTS+=unit-mpusize +TESTS+=unit-flash-erase-h7 # 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 @@ -262,6 +263,11 @@ unit-max-space: ../../include/target.h unit-max-space.c 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-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; +} From eaa6e4201a2d13d2453c339acdfc739c296057d5 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 10:11:27 +0200 Subject: [PATCH 07/12] F-4973: clamp TPM-supplied nvPublic.dataSize before NV read-back in rot.c The TPM-bus-supplied UINT16 nvPublic.dataSize was assigned to digestSz and forwarded to wolfTPM2_NVReadAuth as the read byte count with no bound check. wolfTPM2_NVReadAuthPolicy uses that count as the XMEMCPY length into the caller's buffer with no separate capacity argument, so a malicious/emulated TPM (or a pre-existing NV index larger than the hash) reporting dataSize > 64 overflows the 64-byte digest[WC_MAX_DIGEST_SIZE] stack buffer. Clamp digestSz to sizeof(digest) before the read. Stored values are key-hash digests (<= WC_MAX_DIGEST_SIZE), so the clamp never truncates valid data. Add a unit-rot-auth case driving nvPublic.dataSize=1000 through the existing mocked harness, asserting the requested read count is clamped to the buffer. --- tools/tpm/rot.c | 6 +++++ tools/unit-tests/unit-rot-auth.c | 39 ++++++++++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/tools/tpm/rot.c b/tools/tpm/rot.c index 9c08d06752..5f8973f7b4 100644 --- a/tools/tpm/rot.c +++ b/tools/tpm/rot.c @@ -161,6 +161,12 @@ static int TPM2_Boot_SecureROT_Example(TPMI_RH_NV_AUTH authHandle, word32 nvBase } if (rc == 0) { digestSz = nvPublic.dataSize; + /* 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); /* Read access */ printf("Reading NV 0x%x public key hash\n", nv.handle.hndl); 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; } From 740383a3f53b932dca2ed1ae3e97274805bd0573 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 10:19:01 +0200 Subject: [PATCH 08/12] F-4790: clamp OTP pubkey_size in keystore_get_size to prevent OOB read keystore_get_size() returned slot->pubkey_size verbatim from the OTP keystore slot with no upper bound. A corrupted or mis-provisioned slot with pubkey_size > KEYSTORE_PUBKEY_SIZE produces a positive value that passes every caller guard (pubkey_sz < 0 / <= 0). The callers in image.c (key_sha256/key_sha384/key_sha3_384 and the ECC verify y-coordinate offset) then read past otp_slot_item_cache, which only holds KEYSTORE_PUBKEY_SIZE pubkey bytes. Reject an out-of-range pubkey_size by returning -1, matching the existing defensive validation of item_count in keystore_num_pubkeys() and the -1 error convention the callers already handle. Add unit-otp-keystore, which compiles flash_otp_keystore.c in isolation and verifies keystore_get_size() rejects oversized slots. --- src/flash_otp_keystore.c | 10 +++ tools/unit-tests/Makefile | 7 ++ tools/unit-tests/unit-otp-keystore.c | 126 +++++++++++++++++++++++++++ 3 files changed, 143 insertions(+) create mode 100644 tools/unit-tests/unit-otp-keystore.c 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/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index a08180b9b9..6bd15d38e0 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -63,6 +63,7 @@ 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 @@ -268,6 +269,12 @@ unit-mpusize: ../../include/target.h unit-mpusize.c 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-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; +} From f193b9c239fdd5fcd73fda9f0cdbbe48092e2830 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 10:24:56 +0200 Subject: [PATCH 09/12] F-4789: fix uint64_t overflow in squashelf range filter segment end In the PT_LOAD range filter, segmentEnd was computed as p_paddr + p_memsz - 1 in uint64_t with no overflow check. Both fields come straight from the (possibly crafted) ELF64 program header. When p_paddr + p_memsz exceeds 2^64 the result wraps below p_paddr: the wrapped end can land back inside the requested range, so a segment whose true span lies entirely outside the range is spuriously included (and the converse can silently drop a valid segment). squashelf would then emit a squashed image that wolfBoot signs and boots. Detect the overflow before computing segmentEnd and treat such a segment as out-of-range. ELF32 cannot overflow a uint64 sum and is unaffected. Adds tools/squashelf/test-range-overflow.py (run via `make test`) which fails before the fix and passes after. --- .gitignore | 1 + tools/squashelf/Makefile | 5 +- tools/squashelf/squashelf.c | 22 +++++- tools/squashelf/test-range-overflow.py | 103 +++++++++++++++++++++++++ 4 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 tools/squashelf/test-range-overflow.py diff --git a/.gitignore b/.gitignore index a75fcf7e9a..659cabc9fc 100644 --- a/.gitignore +++ b/.gitignore @@ -197,6 +197,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/tools/squashelf/Makefile b/tools/squashelf/Makefile index 675c39062c..cb6ec5b505 100644 --- a/tools/squashelf/Makefile +++ b/tools/squashelf/Makefile @@ -18,13 +18,16 @@ else CFLAGS+=$(OPTIMIZE) endif -.PHONY: clean all debug +.PHONY: clean all debug test all: $(TARGET) debug: CFLAGS+=$(DEBUG_FLAGS) debug: all +test: $(TARGET) + python3 test-range-overflow.py ./$(TARGET) + $(TARGET): $(TARGET).o @echo "Building squashelf tool" $(CC) -o $@ $< $(LDFLAGS) $(CFLAGS_EXTRA) diff --git a/tools/squashelf/squashelf.c b/tools/squashelf/squashelf.c index 38b3f41edb..cffb2e12c3 100644 --- a/tools/squashelf/squashelf.c +++ b/tools/squashelf/squashelf.c @@ -560,7 +560,27 @@ int main(int argCount, char** argValues) /* Apply range filter if specified */ if (hasRange) { uint64_t segmentStart = p_paddr; - uint64_t segmentEnd = p_paddr + p_memsz - 1; + uint64_t segmentEnd; + + /* Guard against uint64_t overflow when computing the segment + * end (CWE-190). Both fields come straight from the (possibly + * crafted) program header; if p_paddr + p_memsz - 1 wrapped, the + * range check could spuriously include an out-of-range segment + * or drop a valid one. Treat such a segment as out-of-range. */ + if (p_memsz == 0) { + segmentEnd = p_paddr; + } + else if (p_paddr > 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..db0747a920 --- /dev/null +++ b/tools/squashelf/test-range-overflow.py @@ -0,0 +1,103 @@ +#!/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" + d = tempfile.mkdtemp() + rc = 0 + + # 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() From b541edb6e717055bd9f0f14e1d084ef28674cdf4 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 10:44:53 +0200 Subject: [PATCH 10/12] Ignore generated unit test binaries --- .gitignore | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.gitignore b/.gitignore index 659cabc9fc..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 From d85feeb8b28d3e57f32ca40c3aa442e0e3e2d0f5 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 11:21:57 +0200 Subject: [PATCH 11/12] Address Copilot review comments --- tools/keytools/sign.c | 29 +++++++++---- tools/squashelf/test-range-overflow.py | 41 +++++++++---------- tools/tpm/rot.c | 5 ++- .../unit-sign-delta-cert-inv-off.py | 2 + 4 files changed, 46 insertions(+), 31 deletions(-) diff --git a/tools/keytools/sign.c b/tools/keytools/sign.c index 7370af467c..b23633a7b4 100644 --- a/tools/keytools/sign.c +++ b/tools/keytools/sign.c @@ -2516,16 +2516,27 @@ static int base_diff(const char *f_base, uint8_t *pubkey, uint32_t pubkey_sz, in if (CMD.cert_chain_file != NULL) { struct stat cc_stat; if ((stat(CMD.cert_chain_file, &cc_stat) == 0) && - (cc_stat.st_size >= 0) && - ((uintmax_t)cc_stat.st_size <= (uintmax_t)UINT32_MAX)) { - 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) { - new_size *= 2; + (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; } - CMD.header_sz = new_size; } } } diff --git a/tools/squashelf/test-range-overflow.py b/tools/squashelf/test-range-overflow.py index db0747a920..45a6cf181c 100644 --- a/tools/squashelf/test-range-overflow.py +++ b/tools/squashelf/test-range-overflow.py @@ -72,29 +72,28 @@ def run(squashelf, infile, outfile, rng): def main(): squashelf = sys.argv[1] if len(sys.argv) > 1 else "./squashelf" - d = tempfile.mkdtemp() 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") - # 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") + # 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) diff --git a/tools/tpm/rot.c b/tools/tpm/rot.c index 5f8973f7b4..9de7bce1e1 100644 --- a/tools/tpm/rot.c +++ b/tools/tpm/rot.c @@ -161,17 +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/unit-sign-delta-cert-inv-off.py b/tools/unit-tests/unit-sign-delta-cert-inv-off.py index 5f09fd5ef1..314ce8482e 100644 --- a/tools/unit-tests/unit-sign-delta-cert-inv-off.py +++ b/tools/unit-tests/unit-sign-delta-cert-inv-off.py @@ -78,6 +78,8 @@ def find_tlv_u32(data, want_type, scan_end): 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(" Date: Tue, 9 Jun 2026 11:37:12 +0200 Subject: [PATCH 12/12] Reduced footprint of `mpusize()` -> new reduced footprint test limits --- src/boot_arm.c | 41 +++++++++++------------------------------ tools/test.mk | 44 ++++++++++++++++++++++---------------------- 2 files changed, 33 insertions(+), 52 deletions(-) diff --git a/src/boot_arm.c b/src/boot_arm.c index 5255492690..43efef9324 100644 --- a/src/boot_arm.c +++ b/src/boot_arm.c @@ -115,36 +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; - if (size <= (128 * 1024)) - return MPUSIZE_128K; - if (size <= (256 * 1024)) - return MPUSIZE_256K; - if (size <= (512 * 1024)) - return MPUSIZE_512K; - if (size <= (1024 * 1024)) - return MPUSIZE_1M; - if (size <= (2 * 1024 * 1024)) - return MPUSIZE_2M; - if (size <= (4 * 1024 * 1024)) - return MPUSIZE_4M; - if (size <= (8 * 1024 * 1024)) - return MPUSIZE_8M; - if (size <= (16 * 1024 * 1024)) - return MPUSIZE_16M; - if (size <= (32 * 1024 * 1024)) - return MPUSIZE_32M; - if (size <= (64 * 1024 * 1024)) - return MPUSIZE_64M; - if (size <= (128 * 1024 * 1024)) - return MPUSIZE_128M; + 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; } 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