diff --git a/.github/workflows/dualbank-emulator-tests.yml b/.github/workflows/dualbank-emulator-tests.yml new file mode 100644 index 0000000000..6e38b7e95c --- /dev/null +++ b/.github/workflows/dualbank-emulator-tests.yml @@ -0,0 +1,34 @@ +name: dualbank-emulator-tests + +on: + push: + pull_request: + +jobs: + stm32u5-dualbank-swap-fallback: + runs-on: ubuntu-latest + container: + image: ghcr.io/wolfssl/wolfboot-ci-m33mu:latest + steps: + - uses: actions/checkout@v4 + + - name: Init submodules + run: | + git config --global --add safe.directory "$GITHUB_WORKSPACE" + git submodule update --init --single-branch + + - name: Configure stm32u5 (non-secure dual-bank, hardware-assisted swap) + run: | + cp config/examples/stm32u5-nonsecure-dualbank.config .config + + - name: Run dual-bank swap fallback test (stm32u5) + working-directory: test-app/emu-test-apps + run: | + ./test-dualbank.sh + + - name: Upload emulator log on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: dualbank-fallback-log + path: test-app/emu-test-apps/stm32u585-dualbank/dualbank_fallback.log diff --git a/hal/stm32u5.c b/hal/stm32u5.c index 2465df1d02..fa8667ae68 100644 --- a/hal/stm32u5.c +++ b/hal/stm32u5.c @@ -207,6 +207,13 @@ int RAMFUNCTION hal_flash_erase(uint32_t address, int len) *cr &= ~FLASH_CR_PER ; return 0; /* Address out of range */ } + /* BKER refers to the physical bank, whatever the SWAP_BANK setting + * (RM0456 7.5.8): invert it when the banks are swapped, so that the + * erased bank is the one currently mapped at the target address. */ + if ((FLASH_OPTR & (FLASH_OPTR_DBANK | FLASH_OPTR_SWAP_BANK)) == + (FLASH_OPTR_DBANK | FLASH_OPTR_SWAP_BANK)) { + bker ^= FLASH_CR_BKER; + } reg = *cr & (~((FLASH_CR_PNB_MASK << FLASH_CR_PNB_SHIFT) | FLASH_CR_BKER)); reg |= ((((p - base) >> 13) << FLASH_CR_PNB_SHIFT) | FLASH_CR_PER | bker ); *cr = reg; diff --git a/test-app/emu-test-apps/stm32u585-dualbank/Makefile b/test-app/emu-test-apps/stm32u585-dualbank/Makefile new file mode 100644 index 0000000000..6df3a2c707 --- /dev/null +++ b/test-app/emu-test-apps/stm32u585-dualbank/Makefile @@ -0,0 +1,55 @@ +# Makefile +# +# 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 ../../../.config + +WOLFBOOT_PARTITION_BOOT_ADDRESS?=0x08010000 +WOLFBOOT_PARTITION_UPDATE_ADDRESS?=0x08110000 +WOLFBOOT_PARTITION_SIZE?=0x30000 +IMAGE_HEADER_SIZE?=1024 + +APP_ORIGIN=$(shell printf '0x%08x' $$(( $(WOLFBOOT_PARTITION_BOOT_ADDRESS) + $(IMAGE_HEADER_SIZE) ))) +APP_SIZE=$(shell printf '0x%08x' $$(( $(WOLFBOOT_PARTITION_SIZE) - $(IMAGE_HEADER_SIZE) ))) +MARKER_ADDR=$(shell printf '0x%08x' $$(( $(WOLFBOOT_PARTITION_BOOT_ADDRESS) + $(WOLFBOOT_PARTITION_SIZE) ))) + +CC=arm-none-eabi-gcc +OBJCOPY?=arm-none-eabi-objcopy + +CFLAGS:=-mcpu=cortex-m33 -mthumb -Os -ffreestanding -fdata-sections -ffunction-sections -g -ggdb +CFLAGS+=-DUPDATE_BASE=$(WOLFBOOT_PARTITION_UPDATE_ADDRESS)u +CFLAGS+=-DMARKER_ADDR=$(MARKER_ADDR)u +LDFLAGS:=-nostdlib -T target.ld -Wl,-gc-sections + +all: app.bin + +target.ld: target.ld.in + sed -e "s/@FLASH_ORIGIN@/$(APP_ORIGIN)/g" \ + -e "s/@FLASH_SIZE@/$(APP_SIZE)/g" $< > $@ + +app.elf: staging_app.c target.ld + $(CC) $(CFLAGS) staging_app.c $(LDFLAGS) -o $@ + +app.bin: app.elf + $(OBJCOPY) -O binary $< $@ + +clean: + rm -f app.elf app.bin image.bin image_v*_signed.bin target.ld + +.PHONY: all clean diff --git a/test-app/emu-test-apps/stm32u585-dualbank/staging_app.c b/test-app/emu-test-apps/stm32u585-dualbank/staging_app.c new file mode 100644 index 0000000000..35484cac95 --- /dev/null +++ b/test-app/emu-test-apps/stm32u585-dualbank/staging_app.c @@ -0,0 +1,132 @@ +/* staging_app.c + * + * Test application for the STM32U585 DUALBANK_SWAP fallback scenario. + * + * 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 + */ + +/* This image is signed as version 2 and pre-loaded into the UPDATE + * partition, with the BOOT partition empty. The test then exercises the + * hardware-assisted bank swap and the fallback path in a single + * emulator session: + * + * boot 1: wolfBoot selects the update (v2 vs empty), verifies it and + * activates SWAP_BANK, then reboots. This app now sits in the + * logical BOOT partition, mapped to physical bank 2. + * boot 2: wolfBoot boots this app. First run (marker empty): program a + * fake v3 image header (valid magic/size/version, no integrity + * TLVs) into the logical UPDATE partition, set the marker and + * reboot. This simulates staging a corrupted update while + * running from bank 2. + * boot 3: wolfBoot picks the fake v3 update, verification fails, and it + * must erase the CORRUPT UPDATE image (physical bank 1) and + * fall back to this image (physical bank 2). Second run of this + * app (marker set): report success with bkpt 0x7f. + * + * With the pre-fix hal_flash_erase (BKER derived from the logical + * address only), boot 3 erases the healthy image instead and the device + * bricks: bkpt 0x7f is never reached. + */ + +#include + +/* Defaults match config/examples/stm32u5-nonsecure-dualbank.config; the + * Makefile overrides them from the current .config. */ +#ifndef UPDATE_BASE +#define UPDATE_BASE 0x08110000u /* WOLFBOOT_PARTITION_UPDATE_ADDRESS */ +#endif +#ifndef MARKER_ADDR +#define MARKER_ADDR 0x08040000u /* BOOT partition end: unused flash */ +#endif + +#define FLASH_REGS 0x40022000u +#define FLASH_NSKEYR (*(volatile uint32_t *)(FLASH_REGS + 0x008u)) +#define FLASH_NSCR (*(volatile uint32_t *)(FLASH_REGS + 0x028u)) + +#define FLASH_KEY1 0x45670123u +#define FLASH_KEY2 0xCDEF89ABu +#define FLASH_CR_PG (1u << 0) + +#define AIRCR (*(volatile uint32_t *)0xE000ED0Cu) +#define AIRCR_RESET 0x05FA0004u + +#define MARKER_VALUE 0xDEADC0DEu + +extern uint32_t _estack; + +static void bkpt_ok(void) +{ + __asm volatile("bkpt #0x7f"); + while (1) { } +} + +static void flash_program_word(uint32_t addr, uint32_t val) +{ + *(volatile uint32_t *)addr = val; +} + +void app_main(void) +{ + if (*(volatile uint32_t *)MARKER_ADDR == MARKER_VALUE) { + /* Second run: wolfBoot erased the corrupt update and fell back + * to this image. */ + bkpt_ok(); + } + + /* First run: stage a fake v3 image in the UPDATE partition. The + * target area is empty flash, so plain programming is enough. + * Header: magic 'WOLF', size 0x400, version TLV (tag 0x0001, len 4, + * value 3). No SHA/signature TLVs, so verification must fail. */ + FLASH_NSKEYR = FLASH_KEY1; + FLASH_NSKEYR = FLASH_KEY2; + FLASH_NSCR |= FLASH_CR_PG; + + flash_program_word(UPDATE_BASE + 0u, 0x464C4F57u); + flash_program_word(UPDATE_BASE + 4u, 0x00000400u); + flash_program_word(UPDATE_BASE + 8u, 0x00040001u); + flash_program_word(UPDATE_BASE + 12u, 0x00000003u); + + flash_program_word(MARKER_ADDR, MARKER_VALUE); + + FLASH_NSCR &= ~FLASH_CR_PG; + + AIRCR = AIRCR_RESET; + while (1) { } +} + +void Reset_Handler(void) +{ + app_main(); + while (1) { } +} + +static void spin(void) +{ + while (1) { } +} + +__attribute__((section(".isr_vector"), used)) +const void * const vector_table[16] = { + (void *)&_estack, + (void *)Reset_Handler, + (void *)spin, (void *)spin, (void *)spin, (void *)spin, + (void *)spin, (void *)spin, (void *)spin, (void *)spin, + (void *)spin, (void *)spin, (void *)spin, (void *)spin, + (void *)spin, (void *)spin, +}; diff --git a/test-app/emu-test-apps/stm32u585-dualbank/target.ld.in b/test-app/emu-test-apps/stm32u585-dualbank/target.ld.in new file mode 100644 index 0000000000..36be98e1c5 --- /dev/null +++ b/test-app/emu-test-apps/stm32u585-dualbank/target.ld.in @@ -0,0 +1,20 @@ +/* Linker template for the STM32U585 dual-bank staging app. + * @FLASH_ORIGIN@ is WOLFBOOT_PARTITION_BOOT_ADDRESS + IMAGE_HEADER_SIZE, + * @FLASH_SIZE@ is WOLFBOOT_PARTITION_SIZE - IMAGE_HEADER_SIZE. */ +MEMORY +{ + FLASH (rx) : ORIGIN = @FLASH_ORIGIN@, LENGTH = @FLASH_SIZE@ + RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 0x00030000 +} + +_estack = ORIGIN(RAM) + LENGTH(RAM) - 16; + +SECTIONS +{ + .text : + { + KEEP(*(.isr_vector)) + *(.text*) + *(.rodata*) + } > FLASH +} diff --git a/test-app/emu-test-apps/test-dualbank.sh b/test-app/emu-test-apps/test-dualbank.sh new file mode 100755 index 0000000000..ece29c5a8e --- /dev/null +++ b/test-app/emu-test-apps/test-dualbank.sh @@ -0,0 +1,165 @@ +#!/usr/bin/env bash + +# test-dualbank.sh +# +# 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 + +# STM32U585 DUALBANK_SWAP fallback test, on the m33mu emulator. +# +# Exercises the hardware-assisted bank swap and the fallback-after- +# verification-failure path in a single emulator session (three boots): +# +# boot 1: BOOT empty, valid v2 in UPDATE. wolfBoot verifies the update, +# activates SWAP_BANK and reboots. +# boot 2: the v2 app (see stm32u585-dualbank/staging_app.c) runs from +# the logical BOOT partition (physical bank 2), stages a fake +# v3 image with a broken integrity record into the logical +# UPDATE partition (physical bank 1), and reboots. +# boot 3: wolfBoot picks the fake v3 update, verification fails, and it +# must erase the corrupt UPDATE image and fall back to v2. The +# v2 app then reports success with bkpt 0x7f. +# +# This is a regression test for the erase bank selection under +# SWAP_BANK (BKER refers to the physical bank on STM32U5, RM0456 +# 7.5.8): with a logical-address-derived BKER, boot 3 erases the +# healthy v2 image instead of the corrupt update and the device bricks. +# The flash trace is also checked so that a regression cannot hide +# behind an accidentally-successful boot. + +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WOLFBOOT_ROOT="${WOLFBOOT_ROOT:-$(cd "$script_dir/../.." && pwd)}" +# The top-level Makefile defaults WOLFBOOT_ROOT to $(PWD), which is the +# caller's directory when invoked via make -C: export the real root so +# tool paths (keygen/sign) resolve correctly wherever this script runs. +export WOLFBOOT_ROOT +EMU_APPS="$WOLFBOOT_ROOT/test-app/emu-test-apps" +EMU_DIR=stm32u585-dualbank +EMU_PATH="$EMU_APPS/$EMU_DIR" +M33MU="${M33MU:-$(command -v m33mu || true)}" + +log() { + echo "==> $*" +} + +die() { + echo "error: $*" >&2 + exit 1 +} + +cfg_get() { + local key="$1" + local val + val="$(grep -m1 -E "^${key}[?]*[:]*=" "$WOLFBOOT_ROOT/.config" 2>/dev/null | sed -E "s/^${key}[?]*[:]*=//" || true)" + echo "${val}" +} + +[[ -n "$M33MU" ]] || die "m33mu not found (set M33MU=/path/to/m33mu)" +[[ -f "$WOLFBOOT_ROOT/.config" ]] || die "missing .config (use config/examples/stm32u5-nonsecure-dualbank.config)" + +TARGET="$(cfg_get TARGET)" +DUALBANK_SWAP="$(cfg_get DUALBANK_SWAP)" +[[ "$TARGET" == "stm32u5" ]] || die "TARGET=$TARGET, this test needs TARGET=stm32u5" +[[ "$DUALBANK_SWAP" == "1" ]] || die "DUALBANK_SWAP=$DUALBANK_SWAP, this test needs DUALBANK_SWAP=1" + +IMAGE_HEADER_SIZE="$(cfg_get IMAGE_HEADER_SIZE)" +IMAGE_HEADER_SIZE="${IMAGE_HEADER_SIZE:-1024}" +BOOT_ADDR="$(cfg_get WOLFBOOT_PARTITION_BOOT_ADDRESS)" +UPDATE_ADDR="$(cfg_get WOLFBOOT_PARTITION_UPDATE_ADDRESS)" +PART_SIZE="$(cfg_get WOLFBOOT_PARTITION_SIZE)" +[[ -n "$BOOT_ADDR" && -n "$UPDATE_ADDR" && -n "$PART_SIZE" ]] || \ + die "missing partition addresses/size in .config" + +ARCH_OFFSET=0x08000000 +BOOT_OFFSET=$((BOOT_ADDR - ARCH_OFFSET)) +UPDATE_OFFSET=$((UPDATE_ADDR - ARCH_OFFSET)) +UPDATE_OFFSET_HEX=$(printf "0x%x" "$UPDATE_OFFSET") + +STDBUF="" +if command -v stdbuf >/dev/null 2>&1; then + STDBUF="stdbuf -oL -eL" +fi + +RUN_LOG="$EMU_PATH/dualbank_fallback.log" + +log "Rebuilding wolfboot.bin" +make -C "$WOLFBOOT_ROOT" clean wolfboot.bin + +log "Building and signing staging app (v2)" +make -C "$EMU_APPS" TARGET=stm32u5 EMU_DIR="$EMU_DIR" EMU_VERSION=2 \ + IMAGE_HEADER_SIZE="$IMAGE_HEADER_SIZE" sign-emu + +[[ -f "$EMU_PATH/image_v2_signed.bin" ]] || die "missing signed staging app" + +log "Running swap + staged-corrupt-update + fallback cycle (3 boots)" +: >"$RUN_LOG" +set +e +M33MU_FLASH_TRACE=1 $STDBUF "$M33MU" --cpu stm32u585 --dualbank \ + "$WOLFBOOT_ROOT/wolfboot.bin" \ + "$EMU_PATH/image_v2_signed.bin:$UPDATE_OFFSET_HEX" \ + --timeout 120 --expect-bkpt 0x7f >"$RUN_LOG" 2>&1 +emu_rc=$? +set -e + +if ! grep -q "\[EXPECT BKPT\] Success" "$RUN_LOG"; then + tail -n 40 "$RUN_LOG" | sed 's/^/ | /' + die "fallback failed: bkpt 0x7f not reached (m33mu rc=$emu_rc)" +fi +log "bkpt 0x7f hit: wolfBoot fell back to the healthy image" + +resets="$(grep -c "System reset requested" "$RUN_LOG" || true)" +[[ "$resets" == "2" ]] || die "expected 2 system resets (swap + staging), got $resets" +log "2 system resets observed (swap activation + staging reboot)" + +log "Checking erase targets in the flash trace" +python3 - "$RUN_LOG" "$BOOT_OFFSET" "$UPDATE_OFFSET" "$PART_SIZE" <<'PY' +import re, sys + +path, boot_s, update_s, size_s = sys.argv[1:5] +boot = int(boot_s, 0) +update = int(update_s, 0) +size = int(size_s, 0) + +erases = [] +for line in open(path, errors="replace"): + m = re.search(r"\[FLASH_ERASE\].*start=0x([0-9a-fA-F]+) len=0x([0-9a-fA-F]+)", line) + if m: + erases.append((int(m.group(1), 16), int(m.group(2), 16))) + +if not erases: + sys.exit("no [FLASH_ERASE] lines found: flash trace missing?") + +# The bug signature: any erase landing inside the BOOT partition, which +# holds the healthy fallback image for the whole session. +hit_boot = [e for e in erases if boot <= e[0] < boot + size] +if hit_boot: + sys.exit("BUG: erase hit the BOOT partition (healthy image): " + + ", ".join(f"0x{s:x}+0x{l:x}" for s, l in hit_boot)) + +# The corrupt update must have been erased (boot 3 fallback path). +hit_update = [e for e in erases if update <= e[0] < update + size] +if not hit_update: + sys.exit("missing erase of the corrupt UPDATE image") + +print(f"==> erase check ok: {len(hit_update)} update-partition erases, " + f"BOOT partition untouched") +PY + +log "ok: stm32u5 dual-bank swap fallback test passed"