From 7baf9e65f78a7bae1898d37f37a502292e2f05be Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 9 Jul 2026 12:09:14 +0200 Subject: [PATCH 1/3] stm32u5: fix erase bank selection when SWAP_BANK is active On STM32U5 the BKER bit in FLASH_NSCR/SECCR always selects the physical bank: the SWAP_BANK option only changes the address mapping of the banks (RM0456 7.5.8, and confirmed on silicon). hal_flash_erase derived BKER from the logical address only, so with SWAP_BANK active every page erase landed in the bank opposite to the one mapped at the target address. In DUALBANK_SWAP mode this broke fallback recovery: when image verification failed while running from bank 2, wolfBoot_erase_partition (PART_UPDATE) erased the healthy image in the active bank instead of the failing update, leaving the device unable to boot. The same mismatch corrupted any erase issued while swapped, including staging a new update from the application. Invert BKER when both DBANK and SWAP_BANK are set, mirroring the STM32H5 HAL which already handles this (RM0481 has the same physical bank semantics). STM32L5 is not affected: RM0438 defines NSBKER as the page number MSB, which follows the mapped address. Verified on the m33mu emulator with hardware-faithful BKER/SWAP_BANK modeling: full cycle (update to bank 2, swap, stage corrupt update, verify failure) now erases the corrupt update and falls back to the healthy image; before this fix the healthy image was erased and the device bricked. --- hal/stm32u5.c | 7 +++++++ 1 file changed, 7 insertions(+) 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; From 3744aae33dfede5ed939267d5bec27757f7de897 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 9 Jul 2026 15:14:51 +0200 Subject: [PATCH 2/3] ci: add stm32u5 dual-bank swap fallback test on m33mu Add a GitHub Actions workflow covering the DUALBANK_SWAP fallback scenario fixed by the previous commit, running on the m33mu emulator (wolfboot-ci-m33mu image, which models the physical-bank BKER/SWAP_BANK semantics of the STM32U5 flash controller). The test drives three boots in a single emulator session with the stm32u5-nonsecure-dualbank config: 1. wolfBoot verifies a valid v2 image in the UPDATE partition, activates SWAP_BANK and reboots; 2. the v2 app, now running from physical bank 2, stages a fake v3 image with a broken integrity record into the logical UPDATE partition and reboots; 3. wolfBoot selects the v3 update, fails verification, and must erase the corrupt update and fall back to v2, which reports success via breakpoint. Besides the success breakpoint, the script checks the emulator flash trace: the UPDATE partition must be erased and the BOOT partition (holding the healthy image) must never be touched, so a regression cannot hide behind an accidentally bootable state. Verified to fail against the pre-fix hal_flash_erase, where boot 3 erases the healthy image and the device bricks. --- .github/workflows/dualbank-emulator-tests.yml | 34 ++++ .../emu-test-apps/stm32u585-dualbank/Makefile | 55 ++++++ .../stm32u585-dualbank/staging_app.c | 132 ++++++++++++++ .../stm32u585-dualbank/target.ld.in | 20 +++ test-app/emu-test-apps/test-dualbank.sh | 161 ++++++++++++++++++ 5 files changed, 402 insertions(+) create mode 100644 .github/workflows/dualbank-emulator-tests.yml create mode 100644 test-app/emu-test-apps/stm32u585-dualbank/Makefile create mode 100644 test-app/emu-test-apps/stm32u585-dualbank/staging_app.c create mode 100644 test-app/emu-test-apps/stm32u585-dualbank/target.ld.in create mode 100755 test-app/emu-test-apps/test-dualbank.sh 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/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..e5c20724b4 --- /dev/null +++ b/test-app/emu-test-apps/test-dualbank.sh @@ -0,0 +1,161 @@ +#!/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)}" +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" From 8609c9b23ba94dbd152e1a04568daf69b353930d Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 9 Jul 2026 16:43:55 +0200 Subject: [PATCH 3/3] ci: fix WOLFBOOT_ROOT resolution in dual-bank fallback test The top-level Makefile defaults WOLFBOOT_ROOT to $(PWD), which is the environment value of the invoking step: under the workflow's working-directory (test-app/emu-test-apps) the keytools paths resolved under the wrong root and keygen was never found, so src/keystore.c was missing and the wolfboot build failed. Export the computed absolute WOLFBOOT_ROOT so the make invocations resolve tool paths correctly wherever the script is started from. --- test-app/emu-test-apps/test-dualbank.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test-app/emu-test-apps/test-dualbank.sh b/test-app/emu-test-apps/test-dualbank.sh index e5c20724b4..ece29c5a8e 100755 --- a/test-app/emu-test-apps/test-dualbank.sh +++ b/test-app/emu-test-apps/test-dualbank.sh @@ -46,6 +46,10 @@ 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"