diff --git a/.github/workflows/test-build-cmake.yml b/.github/workflows/test-build-cmake.yml index 7c1e6091f9..e73ac5c362 100644 --- a/.github/workflows/test-build-cmake.yml +++ b/.github/workflows/test-build-cmake.yml @@ -37,14 +37,14 @@ jobs: - name: Run CMake build for STM32F407-DISCOVERY run: | rm -rf ./build - cmake -B build -DWOLFBOOT_TARGET=stm32f4 -DWOLFBOOT_PARTITION_SIZE=0x20000 -DWOLFBOOT_SECTOR_SIZE=0x20000 -DWOLFBOOT_PARTITION_BOOT_ADDRESS=0x08020000 -DWOLFBOOT_PARTITION_UPDATE_ADDRESS=0x08040000 -DWOLFBOOT_PARTITION_SWAP_ADDRESS=0x08060000 -DCLOCK_SPEED=160000000 -DSTM32_PLLM=8 -DSTM32_PLLN=336 -DSTM32_PLLP=2 -DSTM32_PLLQ=7 + cmake -B build -DWOLFBOOT_TARGET=stm32f4 -DWOLFBOOT_PARTITION_SIZE=0x60000 -DWOLFBOOT_SECTOR_SIZE=0x20000 -DWOLFBOOT_PARTITION_BOOT_ADDRESS=0x08020000 -DWOLFBOOT_PARTITION_UPDATE_ADDRESS=0x08080000 -DWOLFBOOT_PARTITION_SWAP_ADDRESS=0x080E0000 -DCLOCK_SPEED=160000000 -DSTM32_PLLM=8 -DSTM32_PLLN=336 -DSTM32_PLLP=2 -DSTM32_PLLQ=7 - name: Build wolfBoot run: make -C build - name: Run CMake build for STM32F411-BLACKPILL run: | rm -rf ./build - cmake -B build -DWOLFBOOT_TARGET=stm32f4 -DWOLFBOOT_PARTITION_SIZE=0x20000 -DWOLFBOOT_SECTOR_SIZE=0x20000 -DWOLFBOOT_PARTITION_BOOT_ADDRESS=0x08020000 -DWOLFBOOT_PARTITION_UPDATE_ADDRESS=0x08040000 -DWOLFBOOT_PARTITION_SWAP_ADDRESS=0x08060000 -DCLOCK_SPEED=84000000 -DSTM32_PLLM=25 -DSTM32_PLLN=336 -DSTM32_PLLP=4 -DSTM32_PLLQ=7 + cmake -B build -DWOLFBOOT_TARGET=stm32f4 -DCUSTOM_PARTITION_TRAILER=yes -DWOLFBOOT_PARTITION_SIZE=0x20000 -DWOLFBOOT_SECTOR_SIZE=0x20000 -DWOLFBOOT_PARTITION_BOOT_ADDRESS=0x08020000 -DWOLFBOOT_PARTITION_UPDATE_ADDRESS=0x08040000 -DWOLFBOOT_PARTITION_SWAP_ADDRESS=0x08060000 -DCLOCK_SPEED=84000000 -DSTM32_PLLM=25 -DSTM32_PLLN=336 -DSTM32_PLLP=4 -DSTM32_PLLQ=7 - name: Build wolfBoot run: make -C build diff --git a/.github/workflows/test-renode-blackpill-custom-trailer.yml b/.github/workflows/test-renode-blackpill-custom-trailer.yml new file mode 100644 index 0000000000..cb31102ec3 --- /dev/null +++ b/.github/workflows/test-renode-blackpill-custom-trailer.yml @@ -0,0 +1,42 @@ +name: Renode STM32F411 Black Pill custom partition trailer + +on: + push: + branches: [ 'master', 'main', 'release/**' ] + pull_request: + branches: [ '*' ] + +permissions: + contents: read + packages: read + +jobs: + blackpill-custom-trailer: + runs-on: ubuntu-22.04 + timeout-minutes: 25 + + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Renode update and rollback, single-sector partitions + run: | + docker run --rm -e WORK=/workspace/test_results \ + -v ${{ github.workspace }}:/workspace -w /workspace \ + ghcr.io/wolfssl/wolfboot-ci-renode:v1.5 \ + tools/scripts/renode-test-custom-trailer.sh + + - name: Upload Output Dir + if: always() + uses: actions/upload-artifact@v4 + with: + name: Renode Black Pill custom trailer results + path: test_results/ diff --git a/CMakeLists.txt b/CMakeLists.txt index c905f21c44..bd8dfa743c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -464,6 +464,9 @@ add_option("NO_XIP" "Disable execute-in-place (default: disabled)" "no" "yes;no" add_option( "NVM_FLASH_WRITEONCE" "Enable the workaround for 'write once' internal flash (default: disabled)" "no" "yes;no") +add_option( + "CUSTOM_PARTITION_TRAILER" + "Keep the partition trailers outside the partitions (default: disabled)" "no" "yes;no") add_option( "RAM_CODE" "Move all code accessing internal flash for writing into a section in RAM (default: disabled)" @@ -1088,6 +1091,10 @@ if(NVM_FLASH_WRITEONCE) list(APPEND WOLFBOOT_DEFS NVM_FLASH_WRITEONCE) endif() +if(CUSTOM_PARTITION_TRAILER) + list(APPEND WOLFBOOT_DEFS CUSTOM_PARTITION_TRAILER) +endif() + if(DISABLE_BACKUP) message(WARNING "DISABLE_BACKUP=1 disables power-fail-safe updates; losing power during an update can leave BOOT partially written and unrecoverable") list(APPEND WOLFBOOT_DEFS DISABLE_BACKUP) @@ -1434,6 +1441,10 @@ list(APPEND WOLFBOOT_INCLUDE_DIRS ${WOLFBOOT_ROOT} ${WOLFBOOT_ROOT}/include) # set default linker script set(WOLFBOOT_LSCRIPT_TEMPLATE hal/${WOLFBOOT_TARGET}.ld) +# Partition trailers in sectors 1-2, see hal/stm32f4.c +if(WOLFBOOT_TARGET STREQUAL "stm32f4" AND CUSTOM_PARTITION_TRAILER) + set(WOLFBOOT_LSCRIPT_TEMPLATE hal/stm32f4-trailer.ld) +endif() #--------------------------------------------------------------------------------------------- # wolfcrypt diff --git a/CMakePresets.json b/CMakePresets.json index 2b2ef32334..4047a9b3f5 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -206,13 +206,11 @@ "WOLFBOOT_VERSION": "OFF", "SPMATH": "ON", "RAM_CODE": "OFF", - "WOLFBOOT_PARTITION_SIZE": "0x4000", - "WOLFBOOT_SECTOR_SIZE": "0x4000", - "WOLFBOOT_PARTITION_BOOT_ADDRESS": "0x8000", - "WOLFBOOT_PARTITION_UPDATE_ADDRESS": "0x00000", - "WOLFBOOT_PARTITION_SWAP_ADDRESS": "0x4000", - "WOLFBOOT_LOAD_ADDRESS": "0x200000", - "WOLFBOOT_LOAD_DTS_ADDRESS": "0x400000", + "WOLFBOOT_PARTITION_SIZE": "0x60000", + "WOLFBOOT_SECTOR_SIZE": "0x20000", + "WOLFBOOT_PARTITION_BOOT_ADDRESS": "0x08020000", + "WOLFBOOT_PARTITION_UPDATE_ADDRESS": "0x08080000", + "WOLFBOOT_PARTITION_SWAP_ADDRESS": "0x080E0000", "CLOCK_SPEED": "160000000", "STM32_PLLM": "8", "STM32_PLLN": "336", diff --git a/Makefile b/Makefile index b8a0336af0..4cdaa6cf16 100644 --- a/Makefile +++ b/Makefile @@ -367,6 +367,7 @@ SIGN_ENV=IMAGE_HEADER_SIZE=$(IMAGE_HEADER_SIZE) \ WOLFBOOT_SECTOR_SIZE=$(WOLFBOOT_SECTOR_SIZE) \ NVM_FLASH_WRITEONCE=$(NVM_FLASH_WRITEONCE) \ NVM_FLASH_JOURNAL_DUAL=$(NVM_FLASH_JOURNAL_DUAL) \ + CUSTOM_PARTITION_TRAILER=$(CUSTOM_PARTITION_TRAILER) \ ML_DSA_LEVEL=$(ML_DSA_LEVEL) \ IMAGE_SIGNATURE_SIZE=$(IMAGE_SIGNATURE_SIZE) \ LMS_LEVELS=$(LMS_LEVELS) \ diff --git a/arch.mk b/arch.mk index 8042ff8901..be6e056765 100644 --- a/arch.mk +++ b/arch.mk @@ -508,6 +508,10 @@ ifeq ($(ARCH),ARM) ifeq ($(TARGET),stm32f4) ARCH_FLASH_OFFSET=0x08000000 SPI_TARGET=stm32 + # Partition trailers in sectors 1-2, see hal/stm32f4.c + ifeq ($(CUSTOM_PARTITION_TRAILER),1) + LSCRIPT_IN=hal/stm32f4-trailer.ld + endif endif ifeq ($(TARGET),max32666) diff --git a/config/examples/stm32f4-small-blocks-uart-update.config b/config/examples/stm32f4-small-blocks-uart-update.config index 908bfc4629..324145f20c 100644 --- a/config/examples/stm32f4-small-blocks-uart-update.config +++ b/config/examples/stm32f4-small-blocks-uart-update.config @@ -20,11 +20,11 @@ RAM_CODE?=0 DUALBANK_SWAP?=0 PKA?=1 WOLFTPM?=0 -WOLFBOOT_PARTITION_SIZE?=0x4000 +WOLFBOOT_PARTITION_SIZE?=0x8000 WOLFBOOT_SECTOR_SIZE?=0x4000 WOLFBOOT_PARTITION_BOOT_ADDRESS?=0x8000 WOLFBOOT_PARTITION_UPDATE_ADDRESS?=0x00000 -WOLFBOOT_PARTITION_SWAP_ADDRESS?=0x4000 +WOLFBOOT_PARTITION_SWAP_ADDRESS?=0x8000 WOLFBOOT_LOAD_ADDRESS?=0x200000 WOLFBOOT_LOAD_DTS_ADDRESS?=0x400000 CLOCK_SPEED?=160000000 diff --git a/config/examples/stm32f407-discovery.config b/config/examples/stm32f407-discovery.config index d10b192b2f..b6388a618c 100644 --- a/config/examples/stm32f407-discovery.config +++ b/config/examples/stm32f407-discovery.config @@ -4,11 +4,13 @@ SIGN?=ED25519 HASH?=SHA256 VTOR?=1 SPMATH?=1 -WOLFBOOT_PARTITION_SIZE?=0x20000 +# Trailers in sectors 1-2 (hal/stm32f4.c): the image can fill the partition +CUSTOM_PARTITION_TRAILER?=1 +WOLFBOOT_PARTITION_SIZE?=0x60000 WOLFBOOT_SECTOR_SIZE?=0x20000 WOLFBOOT_PARTITION_BOOT_ADDRESS?=0x08020000 -WOLFBOOT_PARTITION_UPDATE_ADDRESS?=0x08040000 -WOLFBOOT_PARTITION_SWAP_ADDRESS?=0x08060000 +WOLFBOOT_PARTITION_UPDATE_ADDRESS?=0x08080000 +WOLFBOOT_PARTITION_SWAP_ADDRESS?=0x080E0000 CLOCK_SPEED?=160000000 STM32_PLLM?=8 STM32_PLLN?=336 diff --git a/config/examples/stm32f411-blackpill.config b/config/examples/stm32f411-blackpill.config index 3aaffa88d4..d7ae7b27da 100644 --- a/config/examples/stm32f411-blackpill.config +++ b/config/examples/stm32f411-blackpill.config @@ -5,6 +5,8 @@ HASH?=SHA256 VTOR?=1 SPMATH?=1 WOLFBOOT_ORIGIN?=0x08000000 +# Single-sector partitions: the trailers are in sectors 1-2 (hal/stm32f4.c) +CUSTOM_PARTITION_TRAILER?=1 WOLFBOOT_PARTITION_SIZE?=0x20000 WOLFBOOT_SECTOR_SIZE?=0x20000 WOLFBOOT_PARTITION_BOOT_ADDRESS?=0x08020000 diff --git a/docs/flash_partitions.md b/docs/flash_partitions.md index 3373f850ea..a252700978 100644 --- a/docs/flash_partitions.md +++ b/docs/flash_partitions.md @@ -124,12 +124,36 @@ If the `FLAGS_HOME` build option is used then all flags are placed at the end of flag ``` +The trailer sector is erased independently from the image, so it can never hold any part of it, +and the final step of the swap needs one more sector to stage the image tail. For this reason each +partition must be at least two sectors long (three with `NVM_FLASH_WRITEONCE`, which keeps two +copies of the trailer), and the image can use the partition up to the trailer sector(s). The build +fails if `WOLFBOOT_PARTITION_SIZE` is smaller, unless `CUSTOM_PARTITION_TRAILER` is used. + +### Custom partition trailer + You can use the `CUSTOM_PARTITION_TRAILER` option to implement your own functions for: `get_trailer_at`, `set_trailer_at` and `set_partition_magic`. +The trailers are then stored outside of the BOOT and UPDATE partitions, so: + + - the image can use the whole partition, which may be a single sector long; + - triggering an update does not erase anything in the UPDATE partition; + - at the end of the swap, the bootloader resets the sector flags and the UPDATE state itself. + +`set_trailer_at` may be asked to set bits back to 1 (e.g. resetting the flags to `SECT_FLAG_NEW`), +so on flash it has to erase and rewrite the storage where needed. The flash is unlocked when it is called. To enable: -1) Add the `CUSTOM_PARTITION_TRAILER` build option to your `.config`: `CFLAGS_EXTRA+=--DCUSTOM_PARTITION_TRAILER` +1) Add `CUSTOM_PARTITION_TRAILER=1` to your `.config` (or `CFLAGS_EXTRA+=-DCUSTOM_PARTITION_TRAILER`, + in which case also export `CUSTOM_PARTITION_TRAILER=1` when running the `sign` tool, so it allows images as + large as the partition) 2) Add your own .c file using `OBJS_EXTRA`. For example for your own `src/custom_trailer.c` add this to your `.config`: `OBJS_EXTRA=src/custom_trailer.o`. +STM32F4 provides its own implementation in `hal/stm32f4.c`: the boot and update trailers are stored in +the 16KB flash sectors 1 and 2 (0x08004000 and 0x08008000, overridable with +`STM32F4_TRAILER_BOOT_ADDRESS` / `STM32F4_TRAILER_UPDATE_ADDRESS`), and wolfBoot is linked around them +(`hal/stm32f4-trailer.ld`: vector table in sector 0, code from sector 3). This allows the BOOT and UPDATE +partitions to use the 128KB sectors without wasting one of them for each trailer, see +`config/examples/stm32f407-discovery.config` and `config/examples/stm32f411-blackpill.config`. ## Overview of the content of the FLASH partitions diff --git a/hal/stm32f4-trailer.ld b/hal/stm32f4-trailer.ld new file mode 100644 index 0000000000..0de70f7c62 --- /dev/null +++ b/hal/stm32f4-trailer.ld @@ -0,0 +1,64 @@ +/* STM32F4 with CUSTOM_PARTITION_TRAILER: sectors 1 and 2 (16KB each) hold + * the partition trailers (see hal/stm32f4.c), so wolfBoot keeps only its + * vector table in sector 0 and resumes at sector 3. */ +MEMORY +{ + VECTORS (rx) : ORIGIN = 0x08000000, LENGTH = 0x4000 + FLASH (rx) : ORIGIN = 0x0800C000, LENGTH = @BOOTLOADER_PARTITION_SIZE@ - 0xC000 + RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 0x0001C000 +} + +SECTIONS +{ + .isr_vector : + { + _start_text = .; + KEEP(*(.isr_vector)) + . = ALIGN(4); + } > VECTORS + + .text : + { + *(.text*) + *(.rodata*) + . = ALIGN(4); + _end_text = .; + } > FLASH + + .edidx : + { + . = ALIGN(4); + *(.ARM.exidx*) + } > FLASH + + _stored_data = .; + .data : AT (_stored_data) + { + _start_data = .; + KEEP(*(.data*)) + . = ALIGN(4); + KEEP(*(.ramcode)) + . = ALIGN(4); + _end_data = .; + } > RAM + + .bss (NOLOAD) : + { + _start_bss = .; + __bss_start__ = .; + *(.bss*) + *(COMMON) + . = ALIGN(4); + _end_bss = .; + __bss_end__ = .; + _end = .; + } > RAM + . = ALIGN(4); +} + +_wolfboot_partition_boot_address = @WOLFBOOT_PARTITION_BOOT_ADDRESS@; +_wolfboot_partition_size = @WOLFBOOT_PARTITION_SIZE@; +_wolfboot_partition_update_address = @WOLFBOOT_PARTITION_UPDATE_ADDRESS@; +_wolfboot_partition_swap_address = @WOLFBOOT_PARTITION_SWAP_ADDRESS@; + +END_STACK = ORIGIN(RAM) + LENGTH(RAM); diff --git a/hal/stm32f4.c b/hal/stm32f4.c index 06b445a532..51908330cb 100644 --- a/hal/stm32f4.c +++ b/hal/stm32f4.c @@ -19,6 +19,7 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA */ +#include #include #include "image.h" @@ -261,6 +262,85 @@ int RAMFUNCTION hal_flash_erase(uint32_t _address, int len) return 0; } +#ifdef CUSTOM_PARTITION_TRAILER +/* Partition trailers in two dedicated 16KB sectors (1 and 2, a gap left by + * hal/stm32f4-trailer.ld), so the partitions are entirely available for the + * image. Same layout as the default trailer at the end of each sector, and + * bits are cleared in place. Setting bits back (a reset to NEW) erases the + * sector and rewrites the trailer. */ +#ifndef STM32F4_TRAILER_BOOT_ADDRESS +#define STM32F4_TRAILER_BOOT_ADDRESS 0x08004000U +#endif +#ifndef STM32F4_TRAILER_UPDATE_ADDRESS +#define STM32F4_TRAILER_UPDATE_ADDRESS 0x08008000U +#endif +#define STM32F4_TRAILER_SECTOR_SIZE 0x4000U +/* magic + state + sector flags, rewritten on erase */ +#define STM32F4_TRAILER_SIZE 64U + +static uintptr_t RAMFUNCTION trailer_end(uint8_t part) +{ + if (part == PART_BOOT) + return STM32F4_TRAILER_BOOT_ADDRESS + STM32F4_TRAILER_SECTOR_SIZE; + if (part == PART_UPDATE) + return STM32F4_TRAILER_UPDATE_ADDRESS + STM32F4_TRAILER_SECTOR_SIZE; + return 0; +} + +/* Store len bytes ending 'back' bytes before the end of the trailer */ +static void RAMFUNCTION trailer_store(uint8_t part, uint32_t back, + const uint8_t *val, uint32_t len) +{ + uintptr_t end = trailer_end(part); + uint8_t *dst; + uint8_t buf[STM32F4_TRAILER_SIZE]; + uint32_t i; + int rewrite = 0; + + if (end == 0 || back < len || back > STM32F4_TRAILER_SIZE) + return; + dst = (uint8_t *)(end - back); + for (i = 0; i < len; i++) { + if ((val[i] & ~dst[i]) != 0) + rewrite = 1; + } + if (!rewrite) { + hal_flash_write((uint32_t)(uintptr_t)dst, val, (int)len); + } else { + uint8_t *src = (uint8_t *)(end - STM32F4_TRAILER_SIZE); + for (i = 0; i < STM32F4_TRAILER_SIZE; i++) + buf[i] = src[i]; + for (i = 0; i < len; i++) + buf[STM32F4_TRAILER_SIZE - back + i] = val[i]; + hal_flash_erase((uint32_t)(end - STM32F4_TRAILER_SECTOR_SIZE), + STM32F4_TRAILER_SECTOR_SIZE); + hal_flash_write((uint32_t)(uintptr_t)src, buf, STM32F4_TRAILER_SIZE); + } + hal_cache_invalidate(); +} + +uint8_t* RAMFUNCTION get_trailer_at(uint8_t part, uint32_t at) +{ + uintptr_t end = trailer_end(part); + + if (end == 0 || (sizeof(uint32_t) + at) > STM32F4_TRAILER_SIZE) + return NULL; + return (uint8_t *)(end - (sizeof(uint32_t) + at)); +} + +void RAMFUNCTION set_trailer_at(uint8_t part, uint32_t at, uint8_t val) +{ + trailer_store(part, sizeof(uint32_t) + at, &val, 1); +} + +void RAMFUNCTION set_partition_magic(uint8_t part) +{ + uint32_t magic = WOLFBOOT_MAGIC_TRAIL; + + trailer_store(part, sizeof(uint32_t), (uint8_t *)&magic, sizeof(magic)); +} +#endif /* CUSTOM_PARTITION_TRAILER */ + static void clock_pll_off(void) { uint32_t reg32; diff --git a/include/wolfboot/wolfboot.h b/include/wolfboot/wolfboot.h index 0ca0301dec..d8313809e8 100644 --- a/include/wolfboot/wolfboot.h +++ b/include/wolfboot/wolfboot.h @@ -649,8 +649,11 @@ extern "C" { #error "NVM_FLASH_JOURNAL_DUAL needs NVM_FLASH_JOURNAL" #endif -/* Sectors at the end of each partition that hold its trailer */ -#if defined(NVM_FLASH_WRITEONCE) || defined(NVM_FLASH_JOURNAL_DUAL) +/* Sectors at the end of each partition that hold its trailer. A custom + * trailer lives outside the partitions. */ +#if defined(CUSTOM_PARTITION_TRAILER) +#define WOLFBOOT_TRAILER_SECTORS 0 +#elif defined(NVM_FLASH_WRITEONCE) || defined(NVM_FLASH_JOURNAL_DUAL) #define WOLFBOOT_TRAILER_SECTORS 2 #else #define WOLFBOOT_TRAILER_SECTORS 1 diff --git a/options.mk b/options.mk index b1de7043d9..4cf4724a55 100644 --- a/options.mk +++ b/options.mk @@ -1257,6 +1257,10 @@ ifneq ($(FLASH_WRITE_UNIT),) CFLAGS+= -D"WOLFBOOT_FLASH_WRITE_UNIT=$(FLASH_WRITE_UNIT)" endif +ifeq ($(CUSTOM_PARTITION_TRAILER),1) + CFLAGS+= -D"CUSTOM_PARTITION_TRAILER" +endif + ifeq ($(DISABLE_BACKUP),1) $(warning DISABLE_BACKUP=1 disables power-fail-safe updates; losing power during an update can leave BOOT partially written and unrecoverable) CFLAGS+= -D"DISABLE_BACKUP" diff --git a/src/libwolfboot.c b/src/libwolfboot.c index ea574de75e..bcb5d37c10 100644 --- a/src/libwolfboot.c +++ b/src/libwolfboot.c @@ -1573,7 +1573,7 @@ void RAMFUNCTION wolfBoot_erase_partition(uint8_t part) void RAMFUNCTION wolfBoot_update_trigger(void) { uint8_t st = IMG_STATE_UPDATING; -#ifndef NVM_FLASH_JOURNAL +#if !defined(NVM_FLASH_JOURNAL) && !defined(CUSTOM_PARTITION_TRAILER) uintptr_t lastSector = ((PART_UPDATE_ENDFLAGS - 1) / WOLFBOOT_SECTOR_SIZE) * WOLFBOOT_SECTOR_SIZE; #endif #ifdef NVM_FLASH_WRITEONCE @@ -1596,7 +1596,12 @@ void RAMFUNCTION wolfBoot_update_trigger(void) /* NVM_FLASH_WRITEONCE needs erased flags since it selects the fresh * partition based on how many flags are non-erased * FLAGS_INVERT needs erased flags because the bin-assemble's fill byte may - * not match what's in wolfBoot */ + * not match what's in wolfBoot + * A custom trailer does not share a sector with the update image, so + * nothing is erased: it may already hold the image to be installed. */ +#ifdef CUSTOM_PARTITION_TRAILER + wolfBoot_set_partition_state(PART_UPDATE, st); +#else if (FLAGS_UPDATE_EXT()) { ext_flash_erase(lastSector, WOLFBOOT_SECTOR_SIZE); wolfBoot_set_partition_state(PART_UPDATE, st); @@ -1631,6 +1636,7 @@ void RAMFUNCTION wolfBoot_update_trigger(void) nvm_cache_scrub(); #endif } +#endif /* CUSTOM_PARTITION_TRAILER */ #endif /* NVM_FLASH_JOURNAL */ if (FLAGS_UPDATE_EXT()) { diff --git a/src/update_flash.c b/src/update_flash.c index b1cd03ee9e..8118770814 100644 --- a/src/update_flash.c +++ b/src/update_flash.c @@ -43,11 +43,31 @@ #ifdef NVM_FLASH_JOURNAL /* The journal clears its own sectors, so a cut cannot damage the log */ #define wb_trailer_clear(img, part) wolfBoot_clear_trailer(part) +#elif defined(CUSTOM_PARTITION_TRAILER) +/* A custom trailer is outside the partitions: reset the state instead */ +#define wb_trailer_clear(img, part) \ + wolfBoot_set_partition_state(part, IMG_STATE_NEW) #else #define wb_trailer_clear(img, part) \ wb_flash_erase(img, WOLFBOOT_PARTITION_SIZE - TRAILER_LEN, TRAILER_LEN) #endif +/* The trailer sector(s) are erased independently from the image: they must + * not hold any of it, and the final swap needs one more staging sector. */ +#if !defined(CUSTOM_PARTITION_TRAILER) && \ + (WOLFBOOT_PARTITION_SIZE < ((WOLFBOOT_TRAILER_SECTORS + 1) * WOLFBOOT_SECTOR_SIZE)) + #error "WOLFBOOT_PARTITION_SIZE too small: need at least 2 sectors (3 with NVM_FLASH_WRITEONCE or NVM_FLASH_JOURNAL_DUAL), or CUSTOM_PARTITION_TRAILER" +#endif + +/* The swap progress flag of the sector holding the trailer would be erased + * with it, so that sector is not tracked. A custom trailer tracks them all. */ +#ifdef CUSTOM_PARTITION_TRAILER + #define SECTOR_HAS_FLAG(s) (1) +#else + #define SECTOR_HAS_FLAG(s) \ + ((((s) + 1) * WOLFBOOT_SECTOR_SIZE) < WOLFBOOT_PARTITION_SIZE) +#endif + static void wolfBoot_zeroize(void *ptr, size_t len) { volatile uint8_t *p = (volatile uint8_t *)ptr; @@ -732,6 +752,61 @@ static int RAMFUNCTION wolfBoot_swap_and_final_erase(int resume) #endif #endif /* !DISABLE_BACKUP && !CUSTOM_PARTITION_TRAILER */ +#if !defined(DISABLE_BACKUP) && defined(CUSTOM_PARTITION_TRAILER) +/* UPDATE states of a rollback: header versions are mixed during the swap, so + * the direction is recorded when it starts. ROLLBACK, FINAL_FLAGS and + * FINAL_ROLLBACK are reached from the previous state by programming only, so + * they are written in place: a trailer rewrite could be cut half-way. */ +#ifndef WOLFBOOT_FLAGS_INVERT +#define IMG_STATE_ROLLBACK 0x60 +#define IMG_STATE_FINAL_ROLLBACK 0x20 +#else +#define IMG_STATE_ROLLBACK 0x9F +#define IMG_STATE_FINAL_ROLLBACK 0xDF +#endif + +/* Resetting the sector flags rewrites the custom trailer, once per flag: + * UPDATE leaves UPDATING first, or a resumed swap would swap again the + * sectors already reset. BOOT is set before: a cut in the reset then leaves + * at worst an erased trailer, which reads as NEW. */ +static int RAMFUNCTION wolfBoot_custom_trailer_final(int resume, int rollback) +{ + uint8_t updateState; + uint32_t sector; + + if (resume) { + if (wolfBoot_get_partition_state(PART_UPDATE, &updateState) != 0) + return -1; + if (updateState == IMG_STATE_FINAL_FLAGS) + rollback = 0; + else if (updateState == IMG_STATE_FINAL_ROLLBACK) + rollback = 1; + else + return -1; + } + + hal_flash_unlock(); +#ifdef EXT_FLASH + ext_flash_unlock(); +#endif + if (!resume) { + wolfBoot_set_partition_state(PART_UPDATE, + rollback ? IMG_STATE_FINAL_ROLLBACK : IMG_STATE_FINAL_FLAGS); + } + wolfBoot_set_partition_state(PART_BOOT, + rollback ? IMG_STATE_SUCCESS : IMG_STATE_TESTING); + for (sector = 0; (sector * WOLFBOOT_SECTOR_SIZE) < WOLFBOOT_PARTITION_SIZE; + sector++) + wolfBoot_set_update_sector_flag(sector, SECT_FLAG_NEW); + wolfBoot_set_partition_state(PART_UPDATE, IMG_STATE_NEW); +#ifdef EXT_FLASH + ext_flash_lock(); +#endif + hal_flash_lock(); + return 0; +} +#endif /* !DISABLE_BACKUP && CUSTOM_PARTITION_TRAILER */ + #ifdef DELTA_UPDATES #ifndef DELTA_BLOCK_SIZE @@ -1151,9 +1226,6 @@ static int RAMFUNCTION wolfBoot_update(int fallback_allowed) int fallback_image = 0; #ifndef DISABLE_BACKUP int rollback_needed = 0; -#ifdef CUSTOM_PARTITION_TRAILER - (void)rollback_needed; -#endif int bootStateRet = -1; uint8_t bootState = 0; #endif @@ -1239,6 +1311,13 @@ static int RAMFUNCTION wolfBoot_update(int fallback_allowed) #endif wolfBoot_get_update_sector_flag(0, &flag); +#if !defined(DISABLE_BACKUP) && defined(CUSTOM_PARTITION_TRAILER) + if (flag != SECT_FLAG_NEW) { + uint8_t updateState; + rollback_needed = (wolfBoot_get_partition_state(PART_UPDATE, + &updateState) == 0) && (updateState == IMG_STATE_ROLLBACK); + } +#endif /* Check the first sector to detect interrupted update */ if (flag == SECT_FLAG_NEW) { if (((update_type & HDR_IMG_TYPE_PART_MASK) != HDR_IMG_TYPE_APP) || @@ -1362,6 +1441,10 @@ static int RAMFUNCTION wolfBoot_update(int fallback_allowed) #ifdef EXT_FLASH ext_flash_unlock(); #endif +#ifdef CUSTOM_PARTITION_TRAILER + if (rollback_needed) + wolfBoot_set_partition_state(PART_UPDATE, IMG_STATE_ROLLBACK); +#endif /* Interruptible swap * The status is saved in the sector flags of the update partition. * If something goes wrong, the operation will be resumed upon reboot. @@ -1375,7 +1458,7 @@ static int RAMFUNCTION wolfBoot_update(int fallback_allowed) copy_ret = wolfBoot_copy_sector(&update, &swap, sector); if (copy_ret < 0) break; - if (((sector + 1) * sector_size) < WOLFBOOT_PARTITION_SIZE) + if (SECTOR_HAS_FLAG(sector)) wolfBoot_set_update_sector_flag(sector, flag); /* FALL THROUGH */ case SECT_FLAG_SWAPPING: @@ -1401,7 +1484,7 @@ static int RAMFUNCTION wolfBoot_update(int fallback_allowed) } if (copy_ret < 0) break; - if (((sector + 1) * sector_size) < WOLFBOOT_PARTITION_SIZE) + if (SECTOR_HAS_FLAG(sector)) wolfBoot_set_update_sector_flag(sector, flag); /* FALL THROUGH */ case SECT_FLAG_BACKUP: @@ -1412,7 +1495,7 @@ static int RAMFUNCTION wolfBoot_update(int fallback_allowed) copy_ret = wolfBoot_copy_sector(&swap, &boot, sector); if (copy_ret < 0) break; - if (((sector + 1) * sector_size) < WOLFBOOT_PARTITION_SIZE) + if (SECTOR_HAS_FLAG(sector)) wolfBoot_set_update_sector_flag(sector, flag); break; case SECT_FLAG_UPDATED: @@ -1512,8 +1595,7 @@ static int RAMFUNCTION wolfBoot_update(int fallback_allowed) } #endif #else - /* Mark boot partition as TESTING - this tells bootloader to fallback if update fails */ - wolfBoot_set_partition_state(PART_BOOT, IMG_STATE_TESTING); + (void)wolfBoot_custom_trailer_final(0, rollback_needed); #endif #else /* DISABLE_BACKUP */ @@ -1815,6 +1897,9 @@ void RAMFUNCTION wolfBoot_start(void) resumedFinalErase = wolfBoot_swap_and_final_erase(1); if ((resumedFinalErase != 0) || ((bootRet == 0) && (bootState == IMG_STATE_TESTING))) +#elif !defined(DISABLE_BACKUP) + /* resume the final step in case the power failed before it finished */ + if (wolfBoot_custom_trailer_final(1, 0) != 0) #endif { /* Check if the BOOT partition is still in TESTING, diff --git a/test-app/startup_arm.c b/test-app/startup_arm.c index a980a1154a..5f5a5e12b7 100644 --- a/test-app/startup_arm.c +++ b/test-app/startup_arm.c @@ -532,7 +532,7 @@ void (* const IV[])(void) = isr_empty, // FTM2_Fault 87 isr_empty, // FTM2_Ovf_Reload 88 -#elif defined(STM32) /* For STM32 */ +#elif defined(STM32) || defined(TARGET_stm32f4) /* For STM32 */ isr_empty, // NVIC_WWDG_IRQ 0 isr_empty, // PVD_IRQ 1 isr_empty, // TAMP_STAMP_IRQ 2 diff --git a/tools/config.mk b/tools/config.mk index f9eac2bb53..3126a8744c 100644 --- a/tools/config.mk +++ b/tools/config.mk @@ -36,6 +36,7 @@ ifeq ($(ARCH),) NVM_FLASH_JOURNAL?=0 NVM_FLASH_JOURNAL_DUAL?=0 NVM_FLASH_ECC?=0 + CUSTOM_PARTITION_TRAILER?=0 DISABLE_BACKUP?=0 WOLFBOOT_VERSION?=0 V?=0 @@ -119,7 +120,7 @@ WATCHDOG_HANDOFF?=0 CONFIG_VARS:= ARCH TARGET SIGN HASH MCUXSDK MCUXPRESSO MCUXPRESSO_CPU MCUXPRESSO_DRIVERS \ MCUXPRESSO_CMSIS FREEDOM_E_SDK STM32CUBE CYPRESS_PDL CYPRESS_CORE_LIB CYPRESS_TARGET_LIB DEBUG VTOR \ - CORTEX_M0 CORTEX_M7 CORTEX_M23 CORTEX_M33 CORTEX_M55 NO_ASM EXT_FLASH SPI_FLASH NO_XIP UART_FLASH ALLOW_DOWNGRADE NVM_FLASH_WRITEONCE \ + CORTEX_M0 CORTEX_M7 CORTEX_M23 CORTEX_M33 CORTEX_M55 NO_ASM EXT_FLASH SPI_FLASH NO_XIP UART_FLASH ALLOW_DOWNGRADE NVM_FLASH_WRITEONCE CUSTOM_PARTITION_TRAILER \ NVM_FLASH_JOURNAL NVM_FLASH_JOURNAL_DUAL NVM_FLASH_ECC FLASH_WRITE_UNIT \ DISABLE_BACKUP WOLFBOOT_VERSION V NO_MPU ENCRYPT FLAGS_HOME FLAGS_INVERT \ SPMATH SPMATHALL RAM_CODE DUALBANK_SWAP IMAGE_HEADER_SIZE PKA TZEN PSOC6_CRYPTO \ diff --git a/tools/keytools/sign.c b/tools/keytools/sign.c index 097dce8eb8..c1911c2c29 100644 --- a/tools/keytools/sign.c +++ b/tools/keytools/sign.c @@ -2375,6 +2375,9 @@ static int make_header_ex(int is_diff, uint8_t *pubkey, uint32_t pubkey_sz, const char *env_nvm_jd = getenv("NVM_FLASH_JOURNAL_DUAL"); int nvm_two_sectors = (env_nvm_wo && strcmp(env_nvm_wo, "1") == 0) || (env_nvm_jd && strcmp(env_nvm_jd, "1") == 0); + const char *env_custom = getenv("CUSTOM_PARTITION_TRAILER"); + int custom_trailer = (env_custom && *env_custom && + strcmp(env_custom, "1") == 0); errno = 0; tmp = strtoul(env_psize, &endptr, 0); @@ -2403,18 +2406,20 @@ static int make_header_ex(int is_diff, uint8_t *pubkey, uint32_t pubkey_sz, uint32_t total_img_sz = CMD.header_sz + image_sz; /* Only subtract sector for trailer when sector < partition. * When sector >= partition (e.g. update_ram targets), the - * entire partition is available for the image. + * entire partition is available for the image, as it is + * with CUSTOM_PARTITION_TRAILER (trailer outside partitions). * NVM_FLASH_WRITEONCE and NVM_FLASH_JOURNAL_DUAL reserve * 2 sectors. */ + int reserve = (sector_sz < partition_sz) && !custom_trailer; uint32_t trailer_sz = sector_sz; uint32_t max_img_sz; - if (nvm_two_sectors && sector_sz < partition_sz) + if (nvm_two_sectors && reserve) trailer_sz = 2 * sector_sz; - max_img_sz = (sector_sz < partition_sz) ? + max_img_sz = reserve ? (partition_sz - trailer_sz) : partition_sz; if (total_img_sz > max_img_sz) { - if (sector_sz < partition_sz) { + if (reserve) { printf("Error: Image size %u (header %u + firmware %u) " "exceeds max %u (%s %u - %d x sector %u)\n", total_img_sz, CMD.header_sz, image_sz, diff --git a/tools/renode/stm32f4_flash_alias.repl b/tools/renode/stm32f4_flash_alias.repl new file mode 100644 index 0000000000..4518dcc54d --- /dev/null +++ b/tools/renode/stm32f4_flash_alias.repl @@ -0,0 +1,8 @@ +// STM32F4, with the flash also mapped at 0x0 as when booting from +// flash: hal/stm32f4.c programs the flash through this alias. +using "platforms/cpus/stm32f4.repl" + +flash: @ { + sysbus 0x08000000; + sysbus 0x0 + } diff --git a/tools/scripts/renode-test-custom-trailer.sh b/tools/scripts/renode-test-custom-trailer.sh new file mode 100755 index 0000000000..f3e8bc331c --- /dev/null +++ b/tools/scripts/renode-test-custom-trailer.sh @@ -0,0 +1,159 @@ +#!/bin/bash +# Renode test of CUSTOM_PARTITION_TRAILER on the STM32F411 Black Pill layout: +# one 128KB sector per partition, partition trailers in flash sectors 1-2 +# (hal/stm32f4.c), signed image as large as the whole partition. +# +# The update is staged in flash with its trailer set to UPDATING, as the test +# app would leave it. Every boot runs in a fresh Renode instance loaded with +# the flash dumped at the end of the previous one, i.e. a power cycle. +# +# Checks: +# - the build refuses the same layout without CUSTOM_PARTITION_TRAILER +# - the sign tool refuses an image one byte larger than the partition +# - v1 -> v2 (confirmed by the app): v2 boots, and still after a power cycle +# - v1 -> v3 (not confirmed): v3 boots once, then rolls back to v1 +# - wolfBoot itself is not modified, apart from the trailer sectors +# +# Run from the wolfBoot root, e.g. in the CI container: +# docker run --rm -v $PWD:/workspace -w /workspace \ +# ghcr.io/wolfssl/wolfboot-ci-renode:v1.5 \ +# tools/scripts/renode-test-custom-trailer.sh +# Warning: replaces .config and the signing keys. +set -u + +RENODE=${RENODE:-renode} +WORK=${WORK:-/tmp/renode-custom-trailer} +RUN_TIME=${RUN_TIME:-00:00:10} # emulated time per boot +CONFIG=config/examples/stm32f411-blackpill.config +OPTS="UART_FLASH=0 IMAGE_HEADER_SIZE=512" + +FLASH_BASE=0x08000000 +DUMP_SIZE=0x80000 +BOOT=0x08020000 +UPDATE=0x08040000 +PART_SIZE=0x20000 +BOOT_TRAILER=0x08007FFB # state byte, followed by the magic +UPDATE_TRAILER=0x0800BFFB +TRAILER_START=0x4000 # wolfboot.bin offsets of the +TRAILER_END=0xC000 # trailer sectors + +RES=0 +fail() { echo "FAIL: $*"; RES=1; } + +mkdir -p $WORK +rm -f $WORK/* + +cp $CONFIG .config || exit 2 +make keysclean >/dev/null 2>&1 +make keytools >$WORK/build.log 2>&1 || { tail $WORK/build.log; exit 2; } + +echo "== Build without CUSTOM_PARTITION_TRAILER must fail" +make clean >/dev/null 2>&1 +if make $OPTS CUSTOM_PARTITION_TRAILER=0 wolfboot.bin >$WORK/guard.log 2>&1; then + fail "single-sector partitions built without CUSTOM_PARTITION_TRAILER" +elif grep -q "WOLFBOOT_PARTITION_SIZE too small" $WORK/guard.log; then + echo "refused: OK" +else + tail $WORK/guard.log; fail "build failed for another reason" +fi + +echo "== Build with CUSTOM_PARTITION_TRAILER=1" +make clean >/dev/null 2>&1 +make $OPTS wolfboot.bin test-app/image.bin >>$WORK/build.log 2>&1 \ + || { tail -20 $WORK/build.log; exit 2; } + +sign() { # $1 = payload, $2 = version + IMAGE_HEADER_SIZE=512 WOLFBOOT_PARTITION_SIZE=$PART_SIZE \ + WOLFBOOT_SECTOR_SIZE=$PART_SIZE CUSTOM_PARTITION_TRAILER=1 \ + tools/keytools/sign --ed25519 --sha256 $1 \ + wolfboot_signing_private_key.der $2 >>$WORK/sign.log 2>&1 +} + +echo "== Sign images as large as the partition" +cp test-app/image.bin $WORK/big.bin +truncate -s $((PART_SIZE - 512 + 1)) $WORK/big.bin +if sign $WORK/big.bin 1; then + fail "sign accepted an image larger than the partition" +else + echo "partition size + 1 refused: OK" +fi +cp test-app/image.bin $WORK/app.bin +truncate -s $((PART_SIZE - 512)) $WORK/app.bin +for v in 1 2 3; do + sign $WORK/app.bin $v || { tail $WORK/sign.log; exit 2; } +done +[ $(stat -c %s $WORK/app_v1_signed.bin) = $((PART_SIZE)) ] \ + || fail "signed image is not as large as the partition" + +# Boot the flash image $1 in a fresh Renode, dump the flash to $2 when the +# emulated time is over, print the first version reported on the UART. +power_cycle() { + rm -f $WORK/uart.bin $2 + cat > $WORK/boot.resc <>$WORK/renode.log 2>&1 + # the test app reports '*' and its version, big endian + python3 -I - $WORK/uart.bin <<'EOF' +import sys +try: + d = open(sys.argv[1], 'rb').read() +except OSError: + sys.exit(0) +i = d.find(b'*') +while 0 <= i <= len(d) - 5: + if d[i+1:i+4] == b'\0\0\0': + print(d[i+4]) + break + i = d.find(b'*', i + 1) +EOF +} + +byte_at() { od -An -tx1 -j $(($2 - FLASH_BASE)) -N1 $1 | tr -d ' '; } +version_at() { od -An -tu4 -j $(($2 - FLASH_BASE + 12)) -N4 $1 | tr -d ' '; } + +# $1 = update version, $2 = expected version at first boot, $3 = after the +# power cycle, $4 = expected header versions in boot and update partitions +scenario() { + local F=$WORK/factory-v$1.bin A=$WORK/flash-v$1-a.bin B=$WORK/flash-v$1-b.bin V N + echo "== Update v1 -> v$1" + tools/bin-assemble/bin-assemble $F $FLASH_BASE wolfboot.bin \ + $BOOT $WORK/app_v1_signed.bin $UPDATE $WORK/app_v$1_signed.bin \ + >/dev/null 2>&1 || exit 2 + printf '\x70BOOT' | dd of=$F bs=1 seek=$((UPDATE_TRAILER - FLASH_BASE)) \ + conv=notrunc 2>/dev/null + V=$(power_cycle $F $A) + echo "boot 1: version '$V' (expect $2)" + [ "$V" = "$2" ] || fail "v$1 boot 1" + [ -s $A ] || { fail "no flash dump"; return; } + V=$(power_cycle $A $B) + echo "boot 2, after power cycle: version '$V' (expect $3)" + [ "$V" = "$3" ] || fail "v$1 boot 2" + [ -s $B ] || { fail "no flash dump"; return; } + N=$(cmp -l -n $(stat -c %s wolfboot.bin) wolfboot.bin $B | \ + awk -v s=$((TRAILER_START)) -v e=$((TRAILER_END)) \ + '{ a = $1 - 1; if (a < s || a >= e) n++ } END { print n + 0 }') + [ "$N" = 0 ] && echo "wolfBoot intact" || fail "wolfBoot modified: $N bytes" + V="$(version_at $B $BOOT)/$(version_at $B $UPDATE)" + echo "partitions: v$V (expect $4)" + [ "$V" = "$4" ] || fail "v$1 partition contents" + V=$(byte_at $B $BOOT_TRAILER) + echo "boot state: 0x$V (expect 0x00, SUCCESS)" + [ "$V" = 00 ] || fail "v$1 boot state" +} + +scenario 2 2 2 2/1 +scenario 3 3 1 1/3 + +if [ $RES = 0 ]; then echo "custom partition trailer: PASS"; else + echo "custom partition trailer: FAIL"; echo "----- Renode log -----" + tail -n 40 $WORK/renode.log +fi +exit $RES diff --git a/tools/test.mk b/tools/test.mk index 8f39c6e66c..f7d8d82fac 100644 --- a/tools/test.mk +++ b/tools/test.mk @@ -1250,52 +1250,54 @@ test-size-all: # power-fail swap resume (wolfBoot_swap_update_size) in update_flash.c. # Re-measured 2026-10-16: every configuration grew 24-32B more from the # version check added to that resume. - make test-size SIGN=NONE LIMIT=5312 NO_ARM_ASM=1 + # Re-measured 2026-10-28: every configuration grew 28-40B from the + # last-sector erase fix. + make test-size SIGN=NONE LIMIT=5352 NO_ARM_ASM=1 make keysclean - make test-size SIGN=ED25519 LIMIT=12468 NO_ARM_ASM=1 + make test-size SIGN=ED25519 LIMIT=12508 NO_ARM_ASM=1 make keysclean - make test-size SIGN=ECC256 LIMIT=19184 NO_ARM_ASM=1 + make test-size SIGN=ECC256 LIMIT=19224 NO_ARM_ASM=1 make clean - make test-size SIGN=ECC256 NO_ASM=1 LIMIT=14228 NO_ARM_ASM=1 + make test-size SIGN=ECC256 NO_ASM=1 LIMIT=14268 NO_ARM_ASM=1 make keysclean - make test-size SIGN=RSA2048 LIMIT=12020 NO_ARM_ASM=1 + make test-size SIGN=RSA2048 LIMIT=12060 NO_ARM_ASM=1 make clean - make test-size SIGN=RSA2048 NO_ASM=1 LIMIT=12576 NO_ARM_ASM=1 + make test-size SIGN=RSA2048 NO_ASM=1 LIMIT=12616 NO_ARM_ASM=1 make keysclean - make test-size SIGN=RSA4096 LIMIT=12320 NO_ARM_ASM=1 + make test-size SIGN=RSA4096 LIMIT=12360 NO_ARM_ASM=1 make clean - make test-size SIGN=RSA4096 NO_ASM=1 LIMIT=12868 NO_ARM_ASM=1 + make test-size SIGN=RSA4096 NO_ASM=1 LIMIT=12908 NO_ARM_ASM=1 make keysclean - make test-size SIGN=ECC384 LIMIT=19868 NO_ARM_ASM=1 + make test-size SIGN=ECC384 LIMIT=19908 NO_ARM_ASM=1 make clean - make test-size SIGN=ECC384 NO_ASM=1 LIMIT=15584 NO_ARM_ASM=1 + make test-size SIGN=ECC384 NO_ASM=1 LIMIT=15624 NO_ARM_ASM=1 make keysclean - make test-size SIGN=ED448 LIMIT=14480 NO_ARM_ASM=1 + make test-size SIGN=ED448 LIMIT=14520 NO_ARM_ASM=1 make keysclean - make test-size SIGN=RSA3072 LIMIT=12160 NO_ARM_ASM=1 + make test-size SIGN=RSA3072 LIMIT=12200 NO_ARM_ASM=1 make clean - make test-size SIGN=RSA3072 NO_ASM=1 LIMIT=12688 NO_ARM_ASM=1 + make test-size SIGN=RSA3072 NO_ASM=1 LIMIT=12728 NO_ARM_ASM=1 make keysclean - make test-size SIGN=RSAPSS2048 LIMIT=14012 NO_ARM_ASM=1 + make test-size SIGN=RSAPSS2048 LIMIT=14052 NO_ARM_ASM=1 make clean - make test-size SIGN=RSAPSS2048 NO_ASM=1 LIMIT=14568 NO_ARM_ASM=1 + make test-size SIGN=RSAPSS2048 NO_ASM=1 LIMIT=14608 NO_ARM_ASM=1 make keysclean - make test-size SIGN=RSAPSS3072 LIMIT=14176 NO_ARM_ASM=1 + make test-size SIGN=RSAPSS3072 LIMIT=14216 NO_ARM_ASM=1 make clean - make test-size SIGN=RSAPSS3072 NO_ASM=1 LIMIT=14704 NO_ARM_ASM=1 + make test-size SIGN=RSAPSS3072 NO_ASM=1 LIMIT=14744 NO_ARM_ASM=1 make keysclean - make test-size SIGN=RSAPSS4096 LIMIT=14348 NO_ARM_ASM=1 + make test-size SIGN=RSAPSS4096 LIMIT=14388 NO_ARM_ASM=1 make clean - make test-size SIGN=RSAPSS4096 NO_ASM=1 LIMIT=14896 NO_ARM_ASM=1 + make test-size SIGN=RSAPSS4096 NO_ASM=1 LIMIT=14936 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=8324 NO_ARM_ASM=1 + IMAGE_HEADER_SIZE?=5288 LIMIT=8364 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=8980 NO_ARM_ASM=1 + LIMIT=9016 NO_ARM_ASM=1 make keysclean make clean - make test-size SIGN=ML_DSA ML_DSA_LEVEL=2 LIMIT=19902 \ + make test-size SIGN=ML_DSA ML_DSA_LEVEL=2 LIMIT=19930 \ IMAGE_SIGNATURE_SIZE=2420 IMAGE_HEADER_SIZE?=8192 diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index 015740530c..9f12d4ba82 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -68,7 +68,7 @@ TESTS:=unit-parser unit-parser-large-header unit-fdt unit-extflash unit-string \ unit-update-flash-self-update \ unit-update-flash-self-update-ecc \ unit-nsc-update \ - unit-update-flash-enc unit-update-flash-enc-full unit-update-flash-enc-intswap unit-update-ram unit-update-ram-fit unit-update-ram-uboot unit-update-ram-enc unit-update-ram-enc-nopart unit-update-ram-nofixed unit-update-ram-nofixed-noramboot unit-update-ram-noramboot unit-update-ram-custom-trailer unit-custom-trailer-nopart unit-update-flash-hwswap unit-pkcs11_store unit-psa_store unit-wolfhsm_flash_hal unit-disk \ + unit-update-flash-enc unit-update-flash-enc-full unit-update-flash-enc-intswap unit-update-ram unit-update-ram-fit unit-update-ram-uboot unit-update-ram-enc unit-update-ram-enc-nopart unit-update-ram-nofixed unit-update-ram-nofixed-noramboot unit-update-ram-noramboot unit-update-ram-custom-trailer unit-custom-trailer-nopart unit-update-flash-hwswap unit-update-flash-custom-trailer unit-pkcs11_store unit-psa_store unit-wolfhsm_flash_hal unit-disk \ unit-update-disk unit-update-disk-confirm unit-update-disk-golden unit-update-disk-failover unit-update-disk-fsp unit-update-disk-oob unit-update-disk-fit unit-update-disk-fit-ramdisk unit-multiboot unit-boot-x86-fsp unit-loader-tpm-init unit-qspi-flash unit-fwtpm-stub unit-tpm-rsa-exp \ unit-image-nopart unit-image-sha384 unit-image-sha3-384 unit-image-dts \ unit-image-dts-sha384 unit-image-dts-sha3-384 unit-store-sbrk \ @@ -366,6 +366,12 @@ unit-psa_store:CFLAGS+=-I$(WOLFBOOT_LIB_WOLFPSA) -DMOCK_PARTITIONS -DMOCK_KEYVAU unit-update-flash:CFLAGS+=-DMOCK_PARTITIONS -DWOLFBOOT_NO_SIGN -DUNIT_TEST_AUTH \ -DWOLFBOOT_HASH_SHA256 -DPRINTF_ENABLED -DEXT_FLASH -DPART_UPDATE_EXT -DPART_SWAP_EXT \ -DWOLFBOOT_ORIGIN=MOCK_ADDRESS_BOOT -DBOOTLOADER_PARTITION_SIZE=WOLFBOOT_PARTITION_SIZE +# Single-sector partitions (WOLFBOOT_PARTITION_SIZE == WOLFBOOT_SECTOR_SIZE) +# are only valid with the trailers kept outside the partitions. +unit-update-flash-custom-trailer:CFLAGS+=-DMOCK_PARTITIONS -DWOLFBOOT_NO_SIGN -DUNIT_TEST_AUTH \ + -DWOLFBOOT_HASH_SHA256 -DPRINTF_ENABLED -DEXT_FLASH -DPART_UPDATE_EXT -DPART_SWAP_EXT \ + -DWOLFBOOT_ORIGIN=MOCK_ADDRESS_BOOT -DBOOTLOADER_PARTITION_SIZE=WOLFBOOT_PARTITION_SIZE \ + -DCUSTOM_PARTITION_TRAILER -DWOLFBOOT_SECTOR_SIZE=0x8000 unit-nsc-update:CFLAGS+=-DMOCK_PARTITIONS -D__WOLFBOOT -DTZEN \ -DWOLFBOOT_NO_SIGN -DUNIT_TEST_AUTH \ -DWOLFBOOT_HASH_SHA256 -DPRINTF_ENABLED \ @@ -1150,6 +1156,9 @@ unit-update-flash: ../../include/target.h unit-update-flash.c # DISABLE_BACKUP runtime variant: direct update->BOOT copy without the # power-fail-safe swap, to test the update-consumption behavior of that path. +unit-update-flash-custom-trailer: ../../include/target.h unit-update-flash.c + gcc -o $@ unit-update-flash.c ../../src/image.c $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha256.c $(CFLAGS) $(LDFLAGS) + unit-update-flash-db:CFLAGS+=-DMOCK_PARTITIONS -DWOLFBOOT_NO_SIGN -DUNIT_TEST_AUTH \ -DWOLFBOOT_HASH_SHA256 -DPRINTF_ENABLED -DEXT_FLASH -DPART_UPDATE_EXT -DPART_SWAP_EXT \ -DDISABLE_BACKUP \ diff --git a/tools/unit-tests/unit-update-flash.c b/tools/unit-tests/unit-update-flash.c index 0000d11a88..6c39bdbd8f 100644 --- a/tools/unit-tests/unit-update-flash.c +++ b/tools/unit-tests/unit-update-flash.c @@ -57,6 +57,57 @@ const char *argv0; +#ifdef CUSTOM_PARTITION_TRAILER +/* Trailers kept outside the partitions, laid out like the standard ones: + * magic in the last 4 bytes, then state and sector flags going down. */ +#define MOCK_TRAILER_SIZE 64 +static uint8_t mock_trailer[2][MOCK_TRAILER_SIZE]; +#include +/* Power cut injection: the write that brings the countdown to zero is lost. + * Setting bits back erases the trailer first, so a cut then erases it. */ +static int trailer_cut_countdown; +static jmp_buf trailer_cut_jmp; + +static void trailer_store_cut(uint8_t part, uint8_t *dst, const uint8_t *val, + uint32_t len) +{ + uint32_t i; + if (trailer_cut_countdown > 0 && --trailer_cut_countdown == 0) { + for (i = 0; i < len; i++) { + if ((val[i] & ~dst[i]) != 0) { + memset(mock_trailer[part == PART_BOOT ? 0 : 1], 0xFF, + MOCK_TRAILER_SIZE); + break; + } + } + longjmp(trailer_cut_jmp, 1); + } + memcpy(dst, val, len); +} + +uint8_t* get_trailer_at(uint8_t part, uint32_t at) +{ + uint8_t *t = mock_trailer[part == PART_BOOT ? 0 : 1]; + if (at == 0) + return t + MOCK_TRAILER_SIZE - 4; + ck_assert_uint_lt(at, MOCK_TRAILER_SIZE - 4); + return t + MOCK_TRAILER_SIZE - 4 - at; +} + +void set_trailer_at(uint8_t part, uint32_t at, uint8_t val) +{ + ck_assert_msg(!locked || !ext_locked, "Trailer written with FLASH locked"); + trailer_store_cut(part, get_trailer_at(part, at), &val, 1); +} + +void set_partition_magic(uint8_t part) +{ + uint32_t magic = WOLFBOOT_MAGIC_TRAIL; + trailer_store_cut(part, get_trailer_at(part, 0), (uint8_t *)&magic, + sizeof(magic)); +} +#endif + static void reset_mock_stats(void); static void prepare_flash(void); static void cleanup_flash(void); @@ -326,6 +377,9 @@ static void prepare_flash(void) hal_flash_erase(WOLFBOOT_PARTITION_BOOT_ADDRESS, WOLFBOOT_PARTITION_SIZE); hal_flash_erase(WOLFBOOT_PARTITION_UPDATE_ADDRESS, WOLFBOOT_PARTITION_SIZE); hal_flash_lock(); +#ifdef CUSTOM_PARTITION_TRAILER + memset(mock_trailer, 0xFF, sizeof(mock_trailer)); +#endif } static void cleanup_flash(void) @@ -335,6 +389,17 @@ static void cleanup_flash(void) munmap((void *)MOCK_ADDRESS_SWAP, WOLFBOOT_SECTOR_SIZE); } +/* Write the boot trailer: state byte followed by the 4-byte magic */ +static void write_boot_trailer(const uint8_t *trailer) +{ +#ifdef CUSTOM_PARTITION_TRAILER + set_partition_magic(PART_BOOT); + set_trailer_at(PART_BOOT, 1, trailer[0]); +#else + hal_flash_write(PART_BOOT_ENDFLAGS - 5, trailer, 5); +#endif +} + #define DIGEST_TLV_OFF_IN_HDR 28 #ifdef EXT_ENCRYPTED @@ -1081,6 +1146,106 @@ START_TEST (test_disable_backup_update_consumes_update_state) END_TEST #endif /* DISABLE_BACKUP && !CUSTOM_PARTITION_TRAILER */ +#if !defined(DISABLE_BACKUP) && defined(CUSTOM_PARTITION_TRAILER) +/* Cut the power at every custom trailer write of an update. The resumed boot + * must never run the new image confirmed without testing it: either it runs + * in TESTING and rolls back if not confirmed, or the cut came after TESTING + * was set and the rollback already happened. */ +START_TEST (test_custom_trailer_power_cut) +{ + int cut; + int done = 0; + uint8_t st; + + for (cut = 1; !done; cut++) { + reset_mock_stats(); + prepare_flash(); + add_payload(PART_BOOT, 1, TEST_SIZE_SMALL); + add_payload(PART_UPDATE, 2, TEST_SIZE_SMALL); + wolfBoot_update_trigger(); + trailer_cut_countdown = cut; + if (setjmp(trailer_cut_jmp) == 0) { + wolfBoot_start(); + done = 1; + } + else { + locked = ext_locked = 1; + } + trailer_cut_countdown = 0; + if (!done) + wolfBoot_start(); + ck_assert_msg(!wolfBoot_panicked, "cut %d: panic", cut); + ck_assert_msg(wolfBoot_staged_ok, "cut %d: not staged", cut); + ck_assert_int_eq(wolfBoot_get_partition_state(PART_BOOT, &st), 0); + if (wolfBoot_current_firmware_version() == 2) { + ck_assert_msg(st == IMG_STATE_TESTING, "cut %d: BOOT state 0x%x", + cut, st); + ck_assert_msg( + (wolfBoot_get_partition_state(PART_UPDATE, &st) != 0) || + (st == IMG_STATE_NEW), "cut %d: UPDATE state 0x%x", cut, st); + /* not confirmed: falls back */ + wolfBoot_start(); + ck_assert_msg(!wolfBoot_panicked, "cut %d: panic", cut); + ck_assert_msg(wolfBoot_current_firmware_version() == 1, + "cut %d: no rollback", cut); + } + else { + ck_assert_msg(wolfBoot_current_firmware_version() == 1, + "cut %d: version %u", cut, + wolfBoot_current_firmware_version()); + ck_assert_msg(st == IMG_STATE_SUCCESS, "cut %d: BOOT state 0x%x", + cut, st); + } + cleanup_flash(); + } + ck_assert_int_gt(cut, 2); +} +END_TEST + +/* Cut the power at every custom trailer write of a rollback: the image that + * failed to confirm must never come back */ +START_TEST (test_custom_trailer_rollback_power_cut) +{ + int cut; + int done = 0; + uint8_t st; + + for (cut = 1; !done; cut++) { + reset_mock_stats(); + prepare_flash(); + add_payload(PART_BOOT, 1, TEST_SIZE_SMALL); + add_payload(PART_UPDATE, 2, TEST_SIZE_SMALL); + wolfBoot_update_trigger(); + wolfBoot_start(); + ck_assert_uint_eq(wolfBoot_current_firmware_version(), 2); + /* v2 not confirmed: this boot rolls back */ + trailer_cut_countdown = cut; + if (setjmp(trailer_cut_jmp) == 0) { + wolfBoot_start(); + done = 1; + } + else { + locked = ext_locked = 1; + } + trailer_cut_countdown = 0; + if (!done) + wolfBoot_start(); + ck_assert_msg(!wolfBoot_panicked, "cut %d: panic", cut); + ck_assert_msg(wolfBoot_current_firmware_version() == 1, + "cut %d: version %u", cut, wolfBoot_current_firmware_version()); + ck_assert_int_eq(wolfBoot_get_partition_state(PART_BOOT, &st), 0); + ck_assert_msg(st == IMG_STATE_SUCCESS, "cut %d: BOOT state 0x%x", + cut, st); + wolfBoot_start(); + ck_assert_msg(wolfBoot_current_firmware_version() == 1, + "cut %d: rejected image reinstalled", cut); + cleanup_flash(); + } + ck_assert_int_gt(cut, 2); +} +END_TEST +#endif + START_TEST (test_sunnyday_noupdate) { reset_mock_stats(); @@ -1290,7 +1455,14 @@ START_TEST(test_rollback_resume_from_swapping_flag) wolfBoot_start(); ck_assert_uint_eq(wolfBoot_current_firmware_version(), 2); ext_flash_unlock(); +#ifdef CUSTOM_PARTITION_TRAILER + /* written by the rollback before its first sector */ + hal_flash_unlock(); + wolfBoot_set_partition_state(PART_UPDATE, IMG_STATE_ROLLBACK); + hal_flash_lock(); +#else wolfBoot_set_partition_state(PART_UPDATE, IMG_STATE_UPDATING); +#endif ext_flash_lock(); cut_after_sector0_backup_erase(); wolfBoot_start(); @@ -1647,6 +1819,9 @@ START_TEST (test_update_max_size_accepted) } END_TEST +/* With a custom trailer the image can fill the partition: one more byte + * would not fit in it (see test_update_toolarge). */ +#ifndef CUSTOM_PARTITION_TRAILER START_TEST (test_update_max_size_plus_one_rejected) { uint32_t boundary_reject = (uint32_t)(MAX_UPDATE_SIZE + 1U); @@ -1663,6 +1838,7 @@ START_TEST (test_update_max_size_plus_one_rejected) cleanup_flash(); } END_TEST +#endif START_TEST (test_zero_size_update_rejected) { @@ -1710,7 +1886,7 @@ START_TEST (test_emergency_rollback) { hal_flash_unlock(); /* PART_BOOT_ENDFLAGS, not the partition end: in EXT_ENCRYPTED builds * the key/nonce trailer sits between the state trailer and the end. */ - hal_flash_write(PART_BOOT_ENDFLAGS - 5, testing_flags, 5); + write_boot_trailer(testing_flags); hal_flash_lock(); wolfBoot_start(); @@ -1733,7 +1909,7 @@ START_TEST (test_emergency_rollback_equal_versions) { hal_flash_unlock(); /* PART_BOOT_ENDFLAGS, not the partition end: in EXT_ENCRYPTED builds * the key/nonce trailer sits between the state trailer and the end. */ - hal_flash_write(PART_BOOT_ENDFLAGS - 5, testing_flags, 5); + write_boot_trailer(testing_flags); hal_flash_lock(); wolfBoot_start(); @@ -1756,7 +1932,7 @@ START_TEST (test_emergency_rollback_failure_due_to_bad_update) { hal_flash_unlock(); /* PART_BOOT_ENDFLAGS, not the partition end: in EXT_ENCRYPTED builds * the key/nonce trailer sits between the state trailer and the end. */ - hal_flash_write(PART_BOOT_ENDFLAGS - 5, testing_flags, 5); + write_boot_trailer(testing_flags); hal_flash_lock(); /* Corrupt the update */ @@ -1798,7 +1974,8 @@ START_TEST (test_empty_boot_but_update_sha_corrupted_denied) { cleanup_flash(); } -#ifndef DISABLE_BACKUP +/* The final erase is only resumed for a standard trailer */ +#if !defined(DISABLE_BACKUP) && !defined(CUSTOM_PARTITION_TRAILER) START_TEST (test_swap_resume_noop) { reset_mock_stats(); @@ -2388,6 +2565,11 @@ Suite *wolfboot_suite(void) tcase_add_test(forward_update_samesize, test_disable_backup_update_consumes_update_state); #endif +#if !defined(DISABLE_BACKUP) && defined(CUSTOM_PARTITION_TRAILER) + tcase_add_test(forward_update_samesize, test_custom_trailer_power_cut); + tcase_add_test(forward_update_samesize, + test_custom_trailer_rollback_power_cut); +#endif #if defined(PART_SWAP_EXT) && !defined(DISABLE_BACKUP) tcase_add_test(forward_update_samesize, test_update_resume_from_backup_flag); #endif @@ -2418,7 +2600,9 @@ Suite *wolfboot_suite(void) tcase_add_test(update_toolarge, test_update_toolarge); tcase_add_test(update_toolarge, test_update_max_size_minus_one_accepted); tcase_add_test(update_toolarge, test_update_max_size_accepted); +#ifndef CUSTOM_PARTITION_TRAILER tcase_add_test(update_toolarge, test_update_max_size_plus_one_rejected); +#endif tcase_add_test(zero_size_update, test_zero_size_update_rejected); tcase_add_test(invalid_sha, test_invalid_sha); tcase_add_test(emergency_rollback, test_emergency_rollback); @@ -2431,7 +2615,7 @@ Suite *wolfboot_suite(void) tcase_add_test(emergency_rollback_failure_due_to_bad_update, test_emergency_rollback_failure_due_to_bad_update); tcase_add_test(empty_boot_partition_update, test_empty_boot_partition_update); tcase_add_test(empty_boot_but_update_sha_corrupted_denied, test_empty_boot_but_update_sha_corrupted_denied); -#ifndef DISABLE_BACKUP +#if !defined(DISABLE_BACKUP) && !defined(CUSTOM_PARTITION_TRAILER) tcase_add_test(swap_resume, test_swap_resume_noop); #endif tcase_add_test(diffbase_version, test_diffbase_version_reads);