From 16842980c7c612d6fdfe8aa604d268cb30ef95ee Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 1 Oct 2026 10:41:12 -0700 Subject: [PATCH 1/9] Set MSPLIM_S for the Secure main stack at reset --- mk/arch-armv8m.mk | 3 +- mk/common.mk | 4 +++ port/mimxrt700/platform_mimxrt700.c | 4 +++ port/mimxrt700/secure.ld | 4 +++ port/stm32h563/platform_stm32h563.c | 4 +++ src/arch/armv8m/start_armv8m.c | 16 ++++++++--- src/services/wolfhsm/runner/secure.ld | 4 +++ tools/check_stack_seal.py | 41 +++++++++++++++++++++++---- 8 files changed, 69 insertions(+), 11 deletions(-) diff --git a/mk/arch-armv8m.mk b/mk/arch-armv8m.mk index a5959fc3..4a36dc8c 100644 --- a/mk/arch-armv8m.mk +++ b/mk/arch-armv8m.mk @@ -96,6 +96,7 @@ define arch_image_checks @$(TOOLPREFIX)size -A -x $(SECURE_ELF) > $(BUILD_DIR)/sec-sections.txt || \ { echo "FAIL: size on the secure image failed" >&2; exit 1; } @estack=$$(awk '$$3 == "_estack" { print $$1 }' $(BUILD_DIR)/nsc-syms.txt); \ + sstack=$$(awk '$$3 == "_sstack" { print $$1 }' $(BUILD_DIR)/nsc-syms.txt); \ python3 $(ROOT)/tools/check_stack_seal.py --estack "$$estack" \ - $(BUILD_DIR)/sec-sections.txt + --sstack "$$sstack" $(BUILD_DIR)/sec-sections.txt endef diff --git a/mk/common.mk b/mk/common.mk index 35a4a1d4..0d2edda7 100644 --- a/mk/common.mk +++ b/mk/common.mk @@ -28,6 +28,8 @@ WT_MAX_GUESTS ?= 2 # the SP_SMALL math switch; PSPLIM_S faults any real overflow, so this floor # is measured, not guessed. WT_CO_STACK_SIZE ?= 10240 +# Secure main (SPM) stack, sized at link and limited by MSPLIM_S at reset. +WT_SPM_STACK_SIZE ?= 0x4000 WT_LTO ?= 1 ifneq ($(WT_LTO),0) ifneq ($(WT_LTO),1) @@ -1432,6 +1434,7 @@ $(BUILD_MODE_STAMP): FORCE | $(BUILD_DIR) 'WT_BOOTUPDATE_PROBE=$(WT_BOOTUPDATE_PROBE)' \ 'WT_MAX_GUESTS=$(WT_MAX_GUESTS)' \ 'WT_CO_STACK_SIZE=$(WT_CO_STACK_SIZE)' \ + 'WT_SPM_STACK_SIZE=$(WT_SPM_STACK_SIZE)' \ 'WT_WOLFCRYPT_SP_ASM=$(WT_WOLFCRYPT_SP_ASM)' \ 'WT_WOLFCRYPT_ARMASM=$(WT_WOLFCRYPT_ARMASM)' \ 'WT_WOLFCRYPT_STM32_HASH=$(WT_WOLFCRYPT_STM32_HASH)' \ @@ -1535,6 +1538,7 @@ $(SECURE_ELF) $(SECURE_MAP) $(ARCH_LINK_OUTPUTS) &: $(ALL_SECURE_OBJS) $(SECURE_ $(CC) $(SECURE_CFLAGS) \ $(TARGET_LDFLAGS) \ -Wl,--defsym=WT_VNET_DATA_LENGTH=$(WT_VNET_DATA_LENGTH) \ + -Wl,--defsym=WT_SPM_STACK_SIZE=$(WT_SPM_STACK_SIZE) \ -Wl,-T$(SECURE_LD) \ -Wl,--gc-sections -Wl,-Map=$(SECURE_MAP),--cref \ $(WT_LTO_LDFLAGS) $(WT_EXTRA_LDFLAGS) \ diff --git a/port/mimxrt700/platform_mimxrt700.c b/port/mimxrt700/platform_mimxrt700.c index 9c41cfff..bb3463bb 100644 --- a/port/mimxrt700/platform_mimxrt700.c +++ b/port/mimxrt700/platform_mimxrt700.c @@ -482,6 +482,10 @@ void wt_platform_panic(void) void wt_platform_system_reset(void) { + /* Drop the Secure stack limits first: the next boot starts wolfBoot on + * its own stack below MSPLIM_S, and a core that carried the limit over + * the reset would fault its first push. */ + __asm volatile("movs r0, #0\n msr msplim, r0\n msr psplim, r0" ::: "r0"); wt_dsb(); WT_SCB_AIRCR_S = WT_SCB_AIRCR_SYSRESETREQ; wt_dsb(); diff --git a/port/mimxrt700/secure.ld b/port/mimxrt700/secure.ld index 72b1fd2c..5f831408 100644 --- a/port/mimxrt700/secure.ld +++ b/port/mimxrt700/secure.ld @@ -74,6 +74,8 @@ MEMORY { } _estack = ORIGIN(RAM) + LENGTH(RAM); +/* Secure main stack: MSPLIM_S is set to _sstack at reset. */ +_sstack = _estack - WT_SPM_STACK_SIZE; _sidata = LOADADDR(.data); SECTIONS { @@ -321,4 +323,6 @@ SECTIONS { "conformance data/bss overflows into the reserved MMIO holes") ASSERT(ORIGIN(SPSTACKS) == (ORIGIN(CONFDATA) + LENGTH(CONFDATA)), "SP secure stacks must sit directly above the conformance data window") + ASSERT(_ebss <= _sstack, + "secure .bss reaches the SPM main stack (WT_SPM_STACK_SIZE)") } diff --git a/port/stm32h563/platform_stm32h563.c b/port/stm32h563/platform_stm32h563.c index 57511968..3b067ecb 100644 --- a/port/stm32h563/platform_stm32h563.c +++ b/port/stm32h563/platform_stm32h563.c @@ -678,6 +678,10 @@ void wt_platform_system_reset(void) spins < 0x00200000u) { spins++; } + /* Drop the Secure stack limits first: the next boot starts wolfBoot on + * its own stack below MSPLIM_S, and a core that carried the limit over + * the reset would fault its first push. */ + __asm volatile("movs r0, #0\n msr msplim, r0\n msr psplim, r0" ::: "r0"); wt_dsb(); WT_SCB_AIRCR_S = WT_SCB_AIRCR_SYSRESETREQ; wt_dsb(); diff --git a/src/arch/armv8m/start_armv8m.c b/src/arch/armv8m/start_armv8m.c index ca0974c4..aeac4818 100644 --- a/src/arch/armv8m/start_armv8m.c +++ b/src/arch/armv8m/start_armv8m.c @@ -32,7 +32,8 @@ static void wt_reset_main(void) __attribute__((noreturn, used)); /* Naked so nothing is pushed before MSP is moved below the two seal words - * and they are written; MSP moves first so an exception cannot stack over them. */ + * and they are written; MSP moves first so an exception cannot stack over + * them, and MSPLIM_S is set only once MSP sits above it. */ __attribute__((naked, noreturn)) void Reset_Handler(void) { @@ -44,12 +45,16 @@ void Reset_Handler(void) "msr msp, r0 \n" "str r1, [r0, #0] \n" "str r1, [r0, #4] \n" + "ldr r2, =_sstack \n" + "msr msplim, r2 \n" + "isb \n" "b wt_reset_main \n"); } static void wt_reset_main(void) { extern uint32_t _estack; + extern uint32_t _sstack; extern uint32_t _sidata; extern uint32_t _sdata; extern uint32_t _edata; @@ -76,6 +81,7 @@ static void wt_reset_main(void) uint32_t* src = &_sidata; uint32_t* dst = &_sdata; uint32_t msp; + uint32_t msplim; while (dst < &_edata) { *dst++ = *src++; @@ -122,11 +128,13 @@ static void wt_reset_main(void) * boot path behind it. */ __asm__ volatile ("str %1, [%0]" : : "r"(seal), "r"(0u) : "memory"); #endif - /* Refuse to boot unless the reset entry sealed the main stack top and - * moved MSP below the seal words. */ + /* Refuse to boot unless the reset entry sealed the main stack top, moved + * MSP below the seal words, and limited the main stack at _sstack. */ __asm__ volatile ("mrs %0, msp" : "=r"(msp)); + __asm__ volatile ("mrs %0, msplim" : "=r"(msplim)); if (seal[0] != WT_ARMV8M_STACK_SEAL || seal[1] != WT_ARMV8M_STACK_SEAL || - msp > (uint32_t)(uintptr_t)seal) { + msp > (uint32_t)(uintptr_t)seal || + msplim != (uint32_t)(uintptr_t)&_sstack || msplim >= msp) { wt_platform_panic(); } diff --git a/src/services/wolfhsm/runner/secure.ld b/src/services/wolfhsm/runner/secure.ld index 139484ee..b8eae453 100644 --- a/src/services/wolfhsm/runner/secure.ld +++ b/src/services/wolfhsm/runner/secure.ld @@ -68,6 +68,8 @@ MEMORY { } _estack = ORIGIN(RAM) + LENGTH(RAM); +/* Secure main stack: MSPLIM_S is set to _sstack at reset. */ +_sstack = _estack - WT_SPM_STACK_SIZE; _sidata = LOADADDR(.data); SECTIONS { @@ -292,4 +294,6 @@ SECTIONS { "conformance data/bss overflows into the reserved MMIO holes") ASSERT(ORIGIN(SPSTACKS) == (ORIGIN(CONFDATA) + LENGTH(CONFDATA)), "SP secure stacks must sit directly above the conformance data window") + ASSERT(_ebss <= _sstack, + "secure .bss reaches the SPM main stack (WT_SPM_STACK_SIZE)") } diff --git a/tools/check_stack_seal.py b/tools/check_stack_seal.py index a46157fd..fd23f775 100644 --- a/tools/check_stack_seal.py +++ b/tools/check_stack_seal.py @@ -1,5 +1,8 @@ #!/usr/bin/env python3 -"""Check that no allocated section reaches the Secure main-stack seal.""" +"""Check that no allocated section reaches the Secure main stack. + +The seal words sit at the top of the stack; with --sstack the whole stack +[sstack, estack) is reserved, so .bss cannot grow under the stack limit.""" import argparse import re @@ -23,7 +26,7 @@ def sections(lines): return found -def check(lines, estack): +def check(lines, estack, sstack=None): found = sections(lines) if not found: return ["no sections found in the size listing"] @@ -31,11 +34,20 @@ def check(lines, estack): return ["stack top 0x%08x is not a usable 8-byte aligned address" % estack] seal = estack - SEAL_BYTES + if sstack is None: + low = seal + what = "stack seal" + else: + if sstack >= seal or (sstack & 7) != 0: + return ["stack limit 0x%08x is not below the seal at 0x%08x" + % (sstack, seal)] + low = sstack + what = "main stack" errors = [] for name, size, addr in found: - if size != 0 and addr < estack and addr + size > seal: - errors.append("section %s (0x%08x..0x%08x) overlaps the stack " - "seal at 0x%08x" % (name, addr, addr + size, seal)) + if size != 0 and addr < estack and addr + size > low: + errors.append("section %s (0x%08x..0x%08x) overlaps the %s " + "at 0x%08x" % (name, addr, addr + size, what, low)) return errors @@ -67,6 +79,21 @@ def self_test(): if check([], estack) == [] or check(base, 0x30096004) == []: print("self-test accepted unusable input", file=sys.stderr) failures += 1 + limit_cases = ( + ("bss ending at the stack limit", [".bss 0x100 0x30091f00\n"], + True), + ("bss crossing the stack limit", [".bss 0x101 0x30091f00\n"], + False), + ("bss inside the stack", [".bss 0x10 0x30094000\n"], False), + ) + for name, extra, want_ok in limit_cases: + if (check(base + extra, estack, 0x30092000) == []) != want_ok: + print("self-test failed: %s" % name, file=sys.stderr) + failures += 1 + if check(base, estack, 0x30095ff8) == [] or \ + check(base, estack, 0x30092004) == []: + print("self-test accepted an unusable stack limit", file=sys.stderr) + failures += 1 if failures != 0: return 1 print("PASS: stack_seal_layout") @@ -78,6 +105,7 @@ def main(): parser.add_argument("listing", nargs="?", help="output of 'size -A -x' for the Secure image") parser.add_argument("--estack", help="address of the main stack top") + parser.add_argument("--sstack", help="address of the main stack limit") parser.add_argument("--self-test", action="store_true") args = parser.parse_args() @@ -87,8 +115,9 @@ def main(): parser.error("listing and --estack are required") try: estack = int(args.estack, 16) + sstack = int(args.sstack, 16) if args.sstack else None with open(args.listing, "r", errors="replace") as handle: - errors = check(handle, estack) + errors = check(handle, estack, sstack) except (OSError, ValueError) as error: errors = ["cannot check %s: %s" % (args.listing, error)] for error in errors: From 24d2f4b61f16b2831a86ca846928b97ce65fa2f4 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 1 Oct 2026 10:42:16 -0700 Subject: [PATCH 2/9] Halt loudly on HardFault and latch the escalated fault status --- include/wolftrust/arch/armv8m/core_regs.h | 11 +++++ src/arch/armv8m/sp_fault_armv8m.c | 57 +++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/include/wolftrust/arch/armv8m/core_regs.h b/include/wolftrust/arch/armv8m/core_regs.h index b929dc52..38d246c5 100644 --- a/include/wolftrust/arch/armv8m/core_regs.h +++ b/include/wolftrust/arch/armv8m/core_regs.h @@ -34,6 +34,7 @@ #define WT_SCB_SHPR3_S (*(volatile uint32_t*)0xE000ED20u) #define WT_SCB_SHCSR_S (*(volatile uint32_t*)0xE000ED24u) #define WT_SCB_CFSR_S (*(volatile uint32_t*)0xE000ED28u) +#define WT_SCB_HFSR_S (*(volatile uint32_t*)0xE000ED2Cu) #define WT_SCB_MMFAR_S (*(volatile uint32_t*)0xE000ED34u) #define WT_SCB_BFAR_S (*(volatile uint32_t*)0xE000ED38u) #define WT_SCB_ICSR_S (*(volatile uint32_t*)0xE000ED04u) @@ -67,6 +68,16 @@ #define WT_SCB_CFSR_MMFSR_MLSPERR (1u << 5) #define WT_SCB_CFSR_MMFSR_MMARVALID (1u << 7) +#define WT_SCB_CFSR_BFSR_IBUSERR (1u << 8) +#define WT_SCB_CFSR_BFSR_PRECISERR (1u << 9) +#define WT_SCB_CFSR_BFSR_IMPRECISERR (1u << 10) +#define WT_SCB_CFSR_BFSR_UNSTKERR (1u << 11) +#define WT_SCB_CFSR_BFSR_STKERR (1u << 12) +#define WT_SCB_CFSR_BFSR_LSPERR (1u << 13) +#define WT_SCB_CFSR_BFSR_BFARVALID (1u << 15) + +#define WT_SCB_HFSR_FORCED (1u << 30) + #define WT_SCB_CFSR_UFSR_STKOF (1u << 20) /* UFSR bit 4 lifted to CFSR bit 20 */ #define WT_SCB_CFSR_UFSR_NOCP (1u << 19) /* UFSR bit 3: coprocessor disabled */ #define WT_SCB_CFSR_UFSR_UNDEFINSTR (1u << 16) /* UFSR bit 0: undefined instruction */ diff --git a/src/arch/armv8m/sp_fault_armv8m.c b/src/arch/armv8m/sp_fault_armv8m.c index 9a6fdf31..6eab2bda 100644 --- a/src/arch/armv8m/sp_fault_armv8m.c +++ b/src/arch/armv8m/sp_fault_armv8m.c @@ -48,6 +48,63 @@ static volatile uint32_t g_tasklet_fault_icsr; static volatile uint32_t g_tasklet_fault_co; static volatile uint32_t g_tasklet_fault_co_sp; +/* SPM-origin fault latch, read over SWD after the halt: a HardFault, a main + * stack overflow, or a Secure fault with no partition to blame. */ +volatile uint32_t g_wt_spm_fault_cfsr __attribute__((used)); +volatile uint32_t g_wt_spm_fault_hfsr __attribute__((used)); +volatile uint32_t g_wt_spm_fault_mmfar __attribute__((used)); +volatile uint32_t g_wt_spm_fault_bfar __attribute__((used)); +volatile uint32_t g_wt_spm_fault_pc __attribute__((used)); +volatile uint32_t g_wt_spm_fault_exc_return __attribute__((used)); + +/* Stackless: a main-stack overflow or HardFault may arrive with MSP at or + * under its limit, so the latch is written in asm and the halt tail-called. */ +__attribute__((naked, noreturn, used)) +void wt_armv8m_spm_fault_halt(void) +{ + __asm volatile( + "ldr r0, =0xE000ED28 \n" + "ldr r1, [r0, #0] \n" + "ldr r2, =g_wt_spm_fault_cfsr \n" + "str r1, [r2] \n" + "ldr r1, [r0, #4] \n" + "ldr r2, =g_wt_spm_fault_hfsr \n" + "str r1, [r2] \n" + "ldr r1, [r0, #12] \n" + "ldr r2, =g_wt_spm_fault_mmfar \n" + "str r1, [r2] \n" + "ldr r1, [r0, #16] \n" + "ldr r2, =g_wt_spm_fault_bfar \n" + "str r1, [r2] \n" + "ldr r2, =g_wt_spm_fault_exc_return \n" + "str lr, [r2] \n" + /* The stacked PC is read only from a Secure frame that was + * written: STKOF, MSTKERR or STKERR means there is no frame. */ + "ldr r1, [r0, #0] \n" + "ldr r2, =0x00101010 \n" + "tst r1, r2 \n" + "movs r1, #0 \n" + "bne 1f \n" + "tst lr, #0x40 \n" + "beq 1f \n" + "tst lr, #4 \n" + "ite eq \n" + "mrseq r0, msp \n" + "mrsne r0, psp \n" + "ldr r1, [r0, #24] \n" + "1: \n" + "ldr r2, =g_wt_spm_fault_pc \n" + "str r1, [r2] \n" + "b wt_platform_panic \n" + ); +} + +/* Every escalated fault halts here instead of the vector table's mute spin. */ +__attribute__((naked)) void HardFault_Handler(void) +{ + __asm volatile("b wt_armv8m_spm_fault_halt \n"); +} + /* ----------------------------------------------------------------------- * Secure-side tasklet fault path. * From d1f8f2593467861685d7513c42478637db074572 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 1 Oct 2026 10:45:22 -0700 Subject: [PATCH 3/9] Take an SPM main-stack overflow as a platform halt --- mk/arch-armv8m.mk | 4 ++++ mk/common.mk | 1 + src/arch/armv8m/sp_fault_armv8m.c | 14 +++++++++++++- src/arch/armv8m/start_armv8m.c | 4 ++++ tests/host/manifest/test_probe_stamp.sh | 7 ++++--- tests/target/lib/scenario.sh | 11 ++++++++++- tests/target/lib/scenario_matrix.py | 1 + tests/target/run_h5_hardware.sh | 16 +++++++++++++++- tests/target/run_m33mu_scenario.sh | 21 +++++++++++++++++++++ 9 files changed, 73 insertions(+), 6 deletions(-) diff --git a/mk/arch-armv8m.mk b/mk/arch-armv8m.mk index 4a36dc8c..a6e9f690 100644 --- a/mk/arch-armv8m.mk +++ b/mk/arch-armv8m.mk @@ -8,6 +8,7 @@ ARCH_CFLAGS := -mcmse -DWT_TARGET_BUILD=1 # build must carry exactly its one deliberate FP instruction and no other. WT_FP_NEG_PROBE ?= 0 WT_SEAL_NEG_PROBE ?= 0 +WT_MSP_OVF_PROBE ?= 0 ARCH_FP_SCAN_FLAGS := ifeq ($(WT_FP_NEG_PROBE),1) ARCH_CFLAGS += -DWT_FP_NEG_PROBE=1 @@ -16,6 +17,9 @@ endif ifneq ($(filter 1 2 3 4,$(WT_SEAL_NEG_PROBE)),) ARCH_CFLAGS += -DWT_SEAL_NEG_PROBE=$(WT_SEAL_NEG_PROBE) endif +ifeq ($(WT_MSP_OVF_PROBE),1) +ARCH_CFLAGS += -DWT_MSP_OVF_PROBE=1 +endif WT_WOLFCRYPT_SP_ASM ?= 1 WT_WOLFCRYPT_ARMASM ?= 1 diff --git a/mk/common.mk b/mk/common.mk index 0d2edda7..47a3e682 100644 --- a/mk/common.mk +++ b/mk/common.mk @@ -1430,6 +1430,7 @@ $(BUILD_MODE_STAMP): FORCE | $(BUILD_DIR) 'WT_MANIFEST_NEG_PROBE=$(WT_MANIFEST_NEG_PROBE)' \ 'WT_FP_NEG_PROBE=$(WT_FP_NEG_PROBE)' \ 'WT_SEAL_NEG_PROBE=$(WT_SEAL_NEG_PROBE)' \ + 'WT_MSP_OVF_PROBE=$(WT_MSP_OVF_PROBE)' \ 'WT_REMEASURE_PROBE=$(WT_REMEASURE_PROBE)' \ 'WT_BOOTUPDATE_PROBE=$(WT_BOOTUPDATE_PROBE)' \ 'WT_MAX_GUESTS=$(WT_MAX_GUESTS)' \ diff --git a/src/arch/armv8m/sp_fault_armv8m.c b/src/arch/armv8m/sp_fault_armv8m.c index 6eab2bda..6e82e208 100644 --- a/src/arch/armv8m/sp_fault_armv8m.c +++ b/src/arch/armv8m/sp_fault_armv8m.c @@ -256,7 +256,19 @@ __attribute__((naked)) void MemManage_Handler(void) __asm volatile("b wt_armv8m_tasklet_fault_entry \n"); } +/* STKOF on the main stack is the SPM overflowing its own stack: halt before + * anything is pushed, since MSP is already at MSPLIM_S. */ __attribute__((naked)) void UsageFault_Handler(void) { - __asm volatile("b wt_armv8m_tasklet_fault_entry \n"); + __asm volatile( + "ldr r0, =0xE000ED28 \n" + "ldr r1, [r0] \n" + "tst r1, #0x00100000 \n" + "beq 1f \n" + "tst lr, #4 \n" + "bne 1f \n" + "b wt_armv8m_spm_fault_halt \n" + "1: \n" + "b wt_armv8m_tasklet_fault_entry \n" + ); } diff --git a/src/arch/armv8m/start_armv8m.c b/src/arch/armv8m/start_armv8m.c index aeac4818..d0987e27 100644 --- a/src/arch/armv8m/start_armv8m.c +++ b/src/arch/armv8m/start_armv8m.c @@ -138,5 +138,9 @@ static void wt_reset_main(void) wt_platform_panic(); } +#if defined(WT_MSP_OVF_PROBE) && (WT_MSP_OVF_PROBE == 1) + /* mspovfneg: push on the main stack until MSPLIM_S raises STKOF. */ + __asm__ volatile ("1: push {r0-r7}\n b 1b"); +#endif wt_boot_run(); } diff --git a/tests/host/manifest/test_probe_stamp.sh b/tests/host/manifest/test_probe_stamp.sh index c5fff34b..e408b4dd 100644 --- a/tests/host/manifest/test_probe_stamp.sh +++ b/tests/host/manifest/test_probe_stamp.sh @@ -34,10 +34,11 @@ mkdir -p "$build" gen cp "$stamp" "$build/default.stamp" if grep -q '^WT_FP_NEG_PROBE=0$' "$stamp" && - grep -q '^WT_SEAL_NEG_PROBE=0$' "$stamp"; then st=0; else st=1; fi -check $st "default stamp records both probes off" + grep -q '^WT_SEAL_NEG_PROBE=0$' "$stamp" && + grep -q '^WT_MSP_OVF_PROBE=0$' "$stamp"; then st=0; else st=1; fi +check $st "default stamp records every probe off" -for probe in WT_FP_NEG_PROBE WT_SEAL_NEG_PROBE; do +for probe in WT_FP_NEG_PROBE WT_SEAL_NEG_PROBE WT_MSP_OVF_PROBE; do gen "$probe=1" if grep -q "^$probe=1\$" "$stamp" && ! cmp -s "$stamp" "$build/default.stamp"; then st=0; else st=1; fi diff --git a/tests/target/lib/scenario.sh b/tests/target/lib/scenario.sh index fd0deebb..6debebf6 100644 --- a/tests/target/lib/scenario.sh +++ b/tests/target/lib/scenario.sh @@ -48,6 +48,7 @@ scenario_secure_flags() { sealhaltneg) echo "WT_SEAL_NEG_PROBE=2" ;; sealbootneg) echo "WT_SEAL_NEG_PROBE=3" ;; sealpivotneg) echo "WT_SEAL_NEG_PROBE=4" ;; + mspovfneg) echo "WT_MSP_OVF_PROBE=1" ;; *) echo "" ;; esac } @@ -57,7 +58,7 @@ scenario_secure_flags() { scenario_end() { case "$1" in rollbackneg|spbudgetneg) echo "bkpt:0x7d" ;; - manifestneg|manifestneg2|sealbootneg) echo "bkpt:0x7e" ;; + manifestneg|manifestneg2|sealbootneg|mspovfneg) echo "bkpt:0x7e" ;; sealhaltneg) echo "bkpt:0x6e" ;; remeasureneg) echo "bkpt:0x6c" ;; *) echo "idle" ;; @@ -101,6 +102,14 @@ scenario_assert_verdict() { refute_re "the mandatory service never completed a guest lifecycle" \ "$GUEST_DONE_RE" ;; + mspovfneg) + expect_re "main-stack overflow raised STKOF against MSPLIM_S" \ + '\[USGFLT\].*CFSR=0x00[1-9a-f][0-9a-f]0000' + expect "SPM overflow halted the platform fail-closed" \ + "[BKPT] imm=0x7e" + refute_re "no guest scheduled after the refused boot" \ + "$GUEST_STARTED_RE" + ;; *) fail "scenario_assert_verdict: no verdict table for '$1'" ;; diff --git a/tests/target/lib/scenario_matrix.py b/tests/target/lib/scenario_matrix.py index 04342120..cc1b3fda 100755 --- a/tests/target/lib/scenario_matrix.py +++ b/tests/target/lib/scenario_matrix.py @@ -91,6 +91,7 @@ ("sealhaltneg", "Stack seal damage at dispatch halts"), ("sealbootneg", "Damaged main-stack seal refuses to boot"), ("sealpivotneg", "Blocking wait stacked on the seal stays contained"), + ("mspovfneg", "SPM main-stack overflow halts fail-closed"), ), }, "mimxrt700": { diff --git a/tests/target/run_h5_hardware.sh b/tests/target/run_h5_hardware.sh index 2989e3a7..af86382c 100755 --- a/tests/target/run_h5_hardware.sh +++ b/tests/target/run_h5_hardware.sh @@ -42,7 +42,7 @@ set -o pipefail mode="${1:-all}" scenario="${2:-positive}" case "$mode" in build|flash|all) ;; *) echo "usage: $0 build|flash|all [scenario]" >&2; exit 2 ;; esac -case "$scenario" in positive|restart|crossdomain|keystoreneg|panicneg|confboot|devstorage|devcrypto|devattest|devattestqcbor|vaultrecover|vaultrecoversec|authneg|writeonce|hsmattackneg|bootupdate|vnet|vnetneg|gtzcneg|fpneg|sealneg|sealpivotneg|periphneg) ;; *) echo "usage: $0 $mode positive|restart|crossdomain|keystoreneg|panicneg|confboot|devstorage|devcrypto|devattest|devattestqcbor|vaultrecover|vaultrecoversec|authneg|writeonce|hsmattackneg|bootupdate|vnet|vnetneg|gtzcneg|fpneg|sealneg|sealpivotneg|periphneg" >&2; exit 2 ;; esac +case "$scenario" in positive|restart|crossdomain|keystoreneg|panicneg|confboot|devstorage|devcrypto|devattest|devattestqcbor|vaultrecover|vaultrecoversec|authneg|writeonce|hsmattackneg|bootupdate|vnet|vnetneg|gtzcneg|fpneg|sealneg|sealpivotneg|periphneg|mspovfneg) ;; *) echo "usage: $0 $mode positive|restart|crossdomain|keystoreneg|panicneg|confboot|devstorage|devcrypto|devattest|devattestqcbor|vaultrecover|vaultrecoversec|authneg|writeonce|hsmattackneg|bootupdate|vnet|vnetneg|gtzcneg|fpneg|sealneg|sealpivotneg|periphneg|mspovfneg" >&2; exit 2 ;; esac repo="$(cd "$(dirname "$0")/../.." && pwd)" cd "$repo" @@ -171,6 +171,7 @@ if [ "$mode" != "flash" ]; then [ "$scenario" = "fpneg" ] && secure_flags="WT_SP_FAULT_PROBE=1 WT_FP_NEG_PROBE=1" [ "$scenario" = "sealneg" ] && secure_flags="WT_SEAL_NEG_PROBE=1" [ "$scenario" = "sealpivotneg" ] && secure_flags="WT_SEAL_NEG_PROBE=4" + [ "$scenario" = "mspovfneg" ] && secure_flags="WT_MSP_OVF_PROBE=1" [ "$scenario" = "vnet" ] && secure_flags="CONFIG_VNET=y" [ "$scenario" = "vnetneg" ] && secure_flags="CONFIG_VNET=y WT_VNET_NEG_PROBE=1" # WT_CONF_DIAG_TRAP=0: the emulator-only hang-probe fault would become a @@ -881,6 +882,19 @@ if [ "$mode" != "build" ]; then fi expect "guest1 alive through the FP fault" "freertos_guest1: heartbeat" ;; + mspovfneg) + # The reset path pushes on the main stack until MSPLIM_S raises STKOF; + # the stackless halt latches the CFSR (bit 20) before the production + # panic, and no partition or guest ever runs. + cfsr=$(read_secure_u32 g_wt_spm_fault_cfsr) + if [ -n "$cfsr" ] && [ $(( (0x$cfsr >> 20) & 0x1 )) -eq 1 ]; then + check_pass "main-stack overflow raised STKOF against MSPLIM_S (CFSR=0x$cfsr)" + else + check_fail "STKOF" "CFSR 0x${cfsr:-none} lacks STKOF" + fi + refute_re "no guest ran after the halt" \ + '(guest0_psa alive|freertos_guest1: heartbeat)' + ;; sealneg|sealpivotneg) # Software check only: the partition overwrites its own stack-top seal # (sealneg) or blocks with SP parked on it so the SVC frame lands there diff --git a/tests/target/run_m33mu_scenario.sh b/tests/target/run_m33mu_scenario.sh index db613a01..351a832d 100755 --- a/tests/target/run_m33mu_scenario.sh +++ b/tests/target/run_m33mu_scenario.sh @@ -296,6 +296,12 @@ elif [ "$scenario" = "sealbootneg" ]; then # The reset path refuses the damaged main-stack seal before any partition. expect_bkpt=0x7e timeout_s=40 +elif [ "$scenario" = "mspovfneg" ]; then + # The reset path overflows the main stack on purpose; MSPLIM_S raises STKOF + # and the SPM halts on the production panic. Do not quit on the fault. + quit_flag="" + expect_bkpt=0x7e + timeout_s=40 elif [ "$scenario" = "spfaultneg" ] || [ "$scenario" = "panicneg" ] || [ "$scenario" = "vnetneg" ] || [ "$scenario" = "sealneg" ] || [ "$scenario" = "sealpivotneg" ]; then @@ -1004,6 +1010,21 @@ case "$scenario" in expect "run reached the clean scenario end" "[EXPECT BKPT] Success" echo "PASS: target/sealpivotneg" ;; + mspovfneg) + # The SPM pushes on its own stack until MSPLIM_S raises STKOF (CFSR bit + # 20, no partition frame to blame); the stackless halt must land on the + # production panic before any partition or guest runs. + if grep -Eq '\[USGFLT\].*CFSR=0x00[1-9a-f][0-9a-f]0000' "$log"; then + check_pass "main-stack overflow raised STKOF against MSPLIM_S" + else + check_fail "STKOF" "expected a STKOF UsageFault (CFSR bit 20), none seen" + fi + expect "SPM overflow halted the platform fail-closed" "[BKPT] imm=0x7e" + expect "the emulator stopped on that halt" "[EXPECT BKPT] Success" + refute_re "no partition or guest ran after the halt" \ + '(guest0_psa alive|freertos_guest1:|vnet-guest|\[BKPT\] imm=0x7f)' + echo "PASS: target/mspovfneg" + ;; sealhaltneg) # The dedicated BKPT 0x6e separates the seal halt from an unrelated boot # panic (BKPT 0x7e); the damaged partition must never have run. From 45f64c750d6986013af663435b0ae5bb0d37c624 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 1 Oct 2026 10:47:47 -0700 Subject: [PATCH 4/9] Enable BusFault and contain a partition-origin BusFault to the partition --- include/wolftrust/arch/armv8m/core_regs.h | 7 ++ include/wolftrust/platform.h | 4 + mk/common.mk | 5 ++ port/stm32h563/platform_stm32h563.c | 11 +++ src/arch/armv8m/guest_context_armv8m.c | 94 +++++++++++++++++++++-- src/arch/armv8m/sp_fault_armv8m.c | 69 ++++++++++++++--- src/arch/common/spm_gate_core.c | 8 ++ src/spm_partitions.c | 18 +++-- tests/host/manifest/test_probe_stamp.sh | 6 +- tests/target/lib/scenario.sh | 1 + tests/target/run_h5_hardware.sh | 39 +++++++++- 11 files changed, 234 insertions(+), 28 deletions(-) diff --git a/include/wolftrust/arch/armv8m/core_regs.h b/include/wolftrust/arch/armv8m/core_regs.h index 38d246c5..1940d666 100644 --- a/include/wolftrust/arch/armv8m/core_regs.h +++ b/include/wolftrust/arch/armv8m/core_regs.h @@ -43,6 +43,7 @@ #define WT_SCB_AIRCR_VECTKEY (0x05FAu << 16) #define WT_SCB_AIRCR_SYSRESETREQ (WT_SCB_AIRCR_VECTKEY | (1u << 2)) #define WT_SCB_AIRCR_SYSRESETREQS (1u << 3) +#define WT_SCB_AIRCR_BFHFNMINS (1u << 13) /* Config bits that must be preserved across an AIRCR read-modify-write. */ #define WT_SCB_AIRCR_CFG_MASK ((1u << 3) | (1u << 13) | (1u << 14) | (7u << 8)) #define WT_SCB_ICSR_PENDSVCLR (1u << 27) @@ -79,6 +80,12 @@ #define WT_SCB_HFSR_FORCED (1u << 30) #define WT_SCB_CFSR_UFSR_STKOF (1u << 20) /* UFSR bit 4 lifted to CFSR bit 20 */ +/* Faults that leave no readable exception frame at the faulting SP. */ +#define WT_SCB_CFSR_NO_FRAME (WT_SCB_CFSR_MMFSR_MUNSTKERR | \ + WT_SCB_CFSR_MMFSR_MSTKERR | \ + WT_SCB_CFSR_BFSR_UNSTKERR | \ + WT_SCB_CFSR_BFSR_STKERR | \ + WT_SCB_CFSR_UFSR_STKOF) #define WT_SCB_CFSR_UFSR_NOCP (1u << 19) /* UFSR bit 3: coprocessor disabled */ #define WT_SCB_CFSR_UFSR_UNDEFINSTR (1u << 16) /* UFSR bit 0: undefined instruction */ diff --git a/include/wolftrust/platform.h b/include/wolftrust/platform.h index 8957ea9f..e2cbcbde 100644 --- a/include/wolftrust/platform.h +++ b/include/wolftrust/platform.h @@ -32,6 +32,10 @@ typedef struct wt_trap_frame wt_trap_frame_t; #include "wolftrust/types.h" void wt_platform_init(void); +#if defined(WT_BUSFAULT_NEG_PROBE) && (WT_BUSFAULT_NEG_PROBE == 1) +/* busfaultneg: a readable region whose bus returns an error on this SoC. */ +void wt_platform_busfault_probe_region(wt_memory_region_t* region); +#endif /* Only asked of ports providing WT_PORT_CAPABILITY_TZ_FILTER. */ void wt_platform_program_memory_windows(const wt_memory_window_t* windows, size_t count); diff --git a/mk/common.mk b/mk/common.mk index 47a3e682..03dd72d5 100644 --- a/mk/common.mk +++ b/mk/common.mk @@ -86,6 +86,7 @@ WT_SP_FAULT_PROBE ?= 0 WT_HSM_FAULT_PROBE ?= 0 WT_SP_FAULT_ALWAYS_PROBE ?= 0 WT_PANIC_NEG_PROBE ?= 0 +WT_BUSFAULT_NEG_PROBE ?= 0 WT_VNET_NEG_PROBE ?= 0 WT_MANIFEST_NEG_PROBE ?= 0 WT_REMEASURE_PROBE ?= 0 @@ -194,6 +195,9 @@ endif ifeq ($(WT_PANIC_NEG_PROBE),1) SECURE_CFLAGS += -DWT_PANIC_NEG_PROBE=1 endif +ifeq ($(WT_BUSFAULT_NEG_PROBE),1) +SECURE_CFLAGS += -DWT_BUSFAULT_NEG_PROBE=1 +endif ifeq ($(WT_VNET_NEG_PROBE),1) SECURE_CFLAGS += -DWT_VNET_NEG_PROBE=1 endif @@ -1431,6 +1435,7 @@ $(BUILD_MODE_STAMP): FORCE | $(BUILD_DIR) 'WT_FP_NEG_PROBE=$(WT_FP_NEG_PROBE)' \ 'WT_SEAL_NEG_PROBE=$(WT_SEAL_NEG_PROBE)' \ 'WT_MSP_OVF_PROBE=$(WT_MSP_OVF_PROBE)' \ + 'WT_BUSFAULT_NEG_PROBE=$(WT_BUSFAULT_NEG_PROBE)' \ 'WT_REMEASURE_PROBE=$(WT_REMEASURE_PROBE)' \ 'WT_BOOTUPDATE_PROBE=$(WT_BOOTUPDATE_PROBE)' \ 'WT_MAX_GUESTS=$(WT_MAX_GUESTS)' \ diff --git a/port/stm32h563/platform_stm32h563.c b/port/stm32h563/platform_stm32h563.c index 3b067ecb..437ac20f 100644 --- a/port/stm32h563/platform_stm32h563.c +++ b/port/stm32h563/platform_stm32h563.c @@ -265,6 +265,17 @@ volatile void* wt_platform_boot_handoff_region(size_t* size) return (volatile void*)WT_BOOT_HANDOFF_ADDRESS; } +#if defined(WT_BUSFAULT_NEG_PROBE) && (WT_BUSFAULT_NEG_PROBE == 1) +/* The 32 KiB past the end of physical SRAM3 (0x300A0000) is unmapped on the + * H563, so an MPU-permitted read there is a precise BusFault on silicon. */ +void wt_platform_busfault_probe_region(wt_memory_region_t* region) +{ + region->base = 0x300A0000u; + region->size = 0x00008000u; + region->attributes = WT_MEM_ATTR_READ; +} +#endif + static void wt_clock_init(void) { uint32_t reg; diff --git a/src/arch/armv8m/guest_context_armv8m.c b/src/arch/armv8m/guest_context_armv8m.c index d9e3ceaa..46034f72 100644 --- a/src/arch/armv8m/guest_context_armv8m.c +++ b/src/arch/armv8m/guest_context_armv8m.c @@ -139,20 +139,25 @@ static void wt_arch_fp_lockdown(void) void wt_arch_init(void) { wt_arch_fp_lockdown(); - /* Route MemManage and UsageFault to their own handlers (otherwise - * they escalate to HardFault and we lose the fault-status registers - * by the time we get the trap). STKOF on PSPLIM_S overflow surfaces - * as a UsageFault. */ - WT_SCB_SHCSR_S |= WT_SCB_SHCSR_MEMFAULTENA | WT_SCB_SHCSR_USGFAULTENA; + /* Route MemManage, BusFault and UsageFault to their own handlers + * (otherwise they escalate to HardFault and we lose the fault-status + * registers by the time we get the trap). STKOF on a stack limit + * surfaces as a UsageFault. */ + WT_SCB_SHCSR_S |= WT_SCB_SHCSR_MEMFAULTENA | WT_SCB_SHCSR_BUSFAULTENA | + WT_SCB_SHCSR_USGFAULTENA; /* Reset authority belongs to the Secure world. With SYSRESETREQS set, a * Non-secure SYSRESETREQ (e.g. a guest RTOS calling sys_reboot on a fault) * no longer resets the SoC — only Secure code can. This is the correct * Secure-Manager policy and stops a rogue NS reboot from tearing the whole - * system down. Read-modify-write with VECTKEY, preserving the TrustZone - * config bits (PRIS/BFHFNMINS/PRIGROUP). */ + * system down. Keep PRIS/PRIGROUP; clear BFHFNMINS so BusFault, HardFault + * and NMI always target the Secure monitor. */ WT_SCB_AIRCR_S = WT_SCB_AIRCR_VECTKEY | - (WT_SCB_AIRCR_S & WT_SCB_AIRCR_CFG_MASK) | + (WT_SCB_AIRCR_S & WT_SCB_AIRCR_CFG_MASK & + ~WT_SCB_AIRCR_BFHFNMINS) | WT_SCB_AIRCR_SYSRESETREQS; + if ((WT_SCB_AIRCR_S & WT_SCB_AIRCR_BFHFNMINS) != 0u) { + wt_platform_panic(); + } /* PendSV and the secure SysTick must share the lowest priority: SysTick at * the reset default (0, highest) would preempt PendSV mid-coroutine switch, * and a nested exception return off the half-saved frame faults INVPC. @@ -356,6 +361,44 @@ static void wt_secure_fault_dispatch(const wt_trap_frame_t* frame) __builtin_unreachable(); } +/* A Non-secure bus error targets the Secure BusFault (BFHFNMINS is 0): the + * guest that issued it is restarted under its policy, never the platform. */ +static void wt_secure_busfault_dispatch(const wt_trap_frame_t* frame) + __attribute__((noreturn, used)); + +/* A guest (un)stacking error leaves no readable frame; capture this instead. */ +static wt_trap_frame_t g_wt_guest_no_frame; + +static void wt_secure_busfault_dispatch(const wt_trap_frame_t* frame) +{ + uint32_t cfsr = WT_SCB_CFSR_S; + + if ((cfsr & (WT_SCB_CFSR_BFSR_STKERR | WT_SCB_CFSR_BFSR_UNSTKERR)) != 0u) { + frame = &g_wt_guest_no_frame; + } + g_last_fault_address = 0u; + if ((cfsr & WT_SCB_CFSR_BFSR_BFARVALID) != 0u) { + g_last_fault_address = WT_SCB_BFAR_S; + } + WT_SCB_CFSR_S = cfsr & WT_SCB_CFSR_BFSR_MASK; + wt_monitor_on_guest_fault(frame, WT_FAULT_MEMORY_VIOLATION); + wt_platform_panic(); + __builtin_unreachable(); +} + +/* A guest's escalated fault may come from an invalid stack: restart it + * without reading the frame. */ +static void wt_secure_hardfault_dispatch(void) __attribute__((noreturn, used)); + +static void wt_secure_hardfault_dispatch(void) +{ + g_last_fault_address = 0u; + WT_SCB_HFSR_S = WT_SCB_HFSR_FORCED; + wt_monitor_on_guest_fault(&g_wt_guest_no_frame, WT_FAULT_SECURE_ESCALATION); + wt_platform_panic(); + __builtin_unreachable(); +} + void wt_arch_start_secure_timer(uint32_t timeslice_ms) { uint32_t reload; @@ -831,6 +874,41 @@ __attribute__((naked)) void SecureFault_Handler(void) ); } +/* NS-origin BusFault: the SecureFault NS capture, with the frame taken from + * whichever Non-secure stack EXC_RETURN bit 2 names (RTOS threads run on + * PSP_NS; the guest's reset context runs on MSP_NS). */ +__attribute__((naked, used)) void wt_armv8m_guest_busfault_entry(void) +{ + __asm volatile( + "mov r2, sp \n" + "ldr r1, =g_secure_entry_sp \n" + "str r2, [r1] \n" + "ldr r1, =g_live_r4_r11 \n" + "stmia r1!, {r4-r11} \n" + "ldr r1, =g_live_exc_return \n" + "str lr, [r1] \n" + "tst lr, #4 \n" + "ite eq \n" + "mrseq r0, msp_ns \n" + "mrsne r0, psp_ns \n" + "b wt_secure_busfault_dispatch \n" + ); +} + +__attribute__((naked, used)) void wt_armv8m_guest_hardfault_entry(void) +{ + __asm volatile( + "mov r2, sp \n" + "ldr r1, =g_secure_entry_sp \n" + "str r2, [r1] \n" + "ldr r1, =g_live_r4_r11 \n" + "stmia r1!, {r4-r11} \n" + "ldr r1, =g_live_exc_return \n" + "str lr, [r1] \n" + "b wt_secure_hardfault_dispatch \n" + ); +} + __attribute__((naked)) void SysTick_Handler(void) { __asm volatile( diff --git a/src/arch/armv8m/sp_fault_armv8m.c b/src/arch/armv8m/sp_fault_armv8m.c index 6e82e208..731c7eb3 100644 --- a/src/arch/armv8m/sp_fault_armv8m.c +++ b/src/arch/armv8m/sp_fault_armv8m.c @@ -29,9 +29,13 @@ #include "wolftrust/spm_transport.h" #include "wolftrust/ffm.h" #include "wolftrust/sched/coroutine.h" +#include "wolftrust/sched/coroutine_internal.h" #include +#define WT_EXC_RETURN_MODE_THREAD 0x08u +#define WT_EXC_RETURN_SPSEL_PSP 0x04u + #include "wolftrust/sched/tasklet.h" #ifdef WT_ENGINE_HSM #include "wolftrust/services/hsm.h" @@ -78,10 +82,10 @@ void wt_armv8m_spm_fault_halt(void) "str r1, [r2] \n" "ldr r2, =g_wt_spm_fault_exc_return \n" "str lr, [r2] \n" - /* The stacked PC is read only from a Secure frame that was - * written: STKOF, MSTKERR or STKERR means there is no frame. */ + /* The stacked PC is read only from a Secure frame that exists: a + * stack overflow or (un)stacking error means there is no frame. */ "ldr r1, [r0, #0] \n" - "ldr r2, =0x00101010 \n" + "ldr r2, =0x00101818 \n" "tst r1, r2 \n" "movs r1, #0 \n" "bne 1f \n" @@ -99,10 +103,29 @@ void wt_armv8m_spm_fault_halt(void) ); } -/* Every escalated fault halts here instead of the vector table's mute spin. */ +/* A Secure-frame escalation halts; a Non-secure one (BFHFNMINS is 0) is a guest + * escalating its own fault, so only that guest restarts. */ __attribute__((naked)) void HardFault_Handler(void) { - __asm volatile("b wt_armv8m_spm_fault_halt \n"); + __asm volatile( + "tst lr, #0x40 \n" + "beq wt_armv8m_guest_hardfault_entry \n" + "b wt_armv8m_spm_fault_halt \n" + ); +} + +/* C-side twin of the asm latch for a fault the dispatcher attributes to the + * SPM itself (bootstrap thread or a privileged handler frame). */ +static void wt_spm_fault_latch(uint32_t cfsr, const uint32_t *frame, + uint32_t exc_return) +{ + g_wt_spm_fault_cfsr = cfsr; + g_wt_spm_fault_hfsr = WT_SCB_HFSR_S; + g_wt_spm_fault_mmfar = WT_SCB_MMFAR_S; + g_wt_spm_fault_bfar = WT_SCB_BFAR_S; + g_wt_spm_fault_pc = ((cfsr & WT_SCB_CFSR_NO_FRAME) == 0u) ? frame[6] : 0u; + g_wt_spm_fault_exc_return = exc_return; + wt_platform_panic(); } /* ----------------------------------------------------------------------- @@ -150,12 +173,14 @@ static void wt_secure_tasklet_fault_dispatch(uint32_t *frame, uint32_t psp_now; g_tasklet_fault_cfsr = cfsr; - g_tasklet_fault_pc = frame[6]; g_tasklet_fault_exc_return = exc_return; /* Frame position vs the coroutine stack identifies which pusher * built it (SVC/tick 8-word vs NS-preempt callee+signature). */ g_tasklet_fault_frame = (uint32_t)(uintptr_t)frame; - g_tasklet_fault_xpsr = frame[7]; + if ((cfsr & WT_SCB_CFSR_NO_FRAME) == 0u) { + g_tasklet_fault_pc = frame[6]; + g_tasklet_fault_xpsr = frame[7]; + } __asm volatile("mrs %0, psp" : "=r"(psp_now)); g_tasklet_fault_psp = psp_now; g_tasklet_fault_icsr = WT_SCB_ICSR_S; @@ -168,13 +193,24 @@ static void wt_secure_tasklet_fault_dispatch(uint32_t *frame, if ((cfsr & WT_SCB_CFSR_MMFSR_MMARVALID) != 0u) { wt_armv8m_note_fault_address(WT_SCB_MMFAR_S); } + else if ((cfsr & WT_SCB_CFSR_BFSR_BFARVALID) != 0u) { + wt_armv8m_note_fault_address(WT_SCB_BFAR_S); + } /* Write-1-to-clear so the next fault is observable. */ WT_SCB_CFSR_S = cfsr; + /* Only a Secure Thread frame on PSP belongs to a partition: a fault + * raised in a privileged handler (the SVC gate acting for it) or on + * the bootstrap thread is the SPM's own and halts the platform. */ + if ((exc_return & (WT_EXC_RETURN_MODE_THREAD | WT_EXC_RETURN_SPSEL_PSP)) != + (WT_EXC_RETURN_MODE_THREAD | WT_EXC_RETURN_SPSEL_PSP)) { + wt_spm_fault_latch(cfsr, frame, exc_return); + } + wt_tasklet_t *tasklet = wt_tasklet_current(); - if (tasklet == NULL) { + if (tasklet == NULL || tasklet == &g_wt_co_bootstrap) { /* Bootstrap took the fault — no tasklet to abandon. */ - wt_platform_panic(); + wt_spm_fault_latch(cfsr, frame, exc_return); } #if defined(WT_CONFORMANCE) && (WT_CONFORMANCE == 1) @@ -206,8 +242,8 @@ static void wt_secure_tasklet_fault_dispatch(uint32_t *frame, wt_tasklet_mark_faulted(tasklet); } -/* Shared tail for MemManage_Handler and UsageFault_Handler. Naked so - * we control the stack layout the EXC_RETURN unwinds through. */ +/* Shared tail for MemManage, BusFault and UsageFault. Naked so we control + * the stack layout the EXC_RETURN unwinds through. */ __attribute__((naked, used)) void wt_armv8m_tasklet_fault_entry(void) { @@ -256,6 +292,17 @@ __attribute__((naked)) void MemManage_Handler(void) __asm volatile("b wt_armv8m_tasklet_fault_entry \n"); } +/* A Secure-frame BusFault is attributed like MemManage; a Non-secure frame + * (BFHFNMINS is 0, so guest bus errors land here too) is a guest fault. */ +__attribute__((naked)) void BusFault_Handler(void) +{ + __asm volatile( + "tst lr, #0x40 \n" + "beq wt_armv8m_guest_busfault_entry \n" + "b wt_armv8m_tasklet_fault_entry \n" + ); +} + /* STKOF on the main stack is the SPM overflowing its own stack: halt before * anything is pushed, since MSP is already at MSPLIM_S. */ __attribute__((naked)) void UsageFault_Handler(void) diff --git a/src/arch/common/spm_gate_core.c b/src/arch/common/spm_gate_core.c index 1f786142..90f9bc09 100644 --- a/src/arch/common/spm_gate_core.c +++ b/src/arch/common/spm_gate_core.c @@ -1011,6 +1011,14 @@ static int wt_spm_sched_add_common(wt_ffm_runtime_t* runtime, slot->table.regions, region_count, WT_MAX_MEMORY_REGIONS); +#endif +#if defined(WT_BUSFAULT_NEG_PROBE) && (WT_BUSFAULT_NEG_PROBE == 1) + /* busfaultneg: grant the port's bus-error window so the probe read + * passes the MPU and faults on the bus. Never built into production. */ + if (region_count < WT_MAX_MEMORY_REGIONS) { + wt_platform_busfault_probe_region(&slot->table.regions[region_count]); + region_count++; + } #endif slot->table.region_count = region_count; diff --git a/src/spm_partitions.c b/src/spm_partitions.c index 2a7ea91a..5b8a88b9 100644 --- a/src/spm_partitions.c +++ b/src/spm_partitions.c @@ -74,13 +74,21 @@ static void wt_spm_hsm_entry(void* arg) * recovery (WT-FFM-0017/0051). Never built into production images. */ wt_arch_sp_fault_probe(0u); #endif -#if defined(WT_SP_FAULT_PROBE) && (WT_SP_FAULT_PROBE == 1) - /* One-shot graceful-recovery probe (target/spfaultneg): an undefined - * instruction raises a recoverable Secure-Thread UsageFault. The - * recovery re-arms this partition with the restarted marker set, so the - * re-run skips the probe and serves. */ +#if (defined(WT_SP_FAULT_PROBE) && (WT_SP_FAULT_PROBE == 1)) || \ + (defined(WT_BUSFAULT_NEG_PROBE) && (WT_BUSFAULT_NEG_PROBE == 1)) + /* One-shot graceful-recovery probes (target/spfaultneg, busfaultneg): an + * undefined instruction, or a read of the port's bus-error window, raises + * a recoverable Secure-Thread fault. The recovery re-arms this partition + * with the restarted marker set, so the re-run skips the probe. */ if (((intptr_t)arg & WT_SP_FAULT_PROBE_RESTARTED) == 0) { +#if defined(WT_BUSFAULT_NEG_PROBE) && (WT_BUSFAULT_NEG_PROBE == 1) + wt_memory_region_t probe; + + wt_platform_busfault_probe_region(&probe); + (void)*(const volatile uint32_t*)(uintptr_t)probe.base; +#else wt_arch_sp_fault_probe(0u); +#endif } partition_id = (int32_t)((intptr_t)arg & ~(intptr_t)WT_SP_FAULT_PROBE_RESTARTED); diff --git a/tests/host/manifest/test_probe_stamp.sh b/tests/host/manifest/test_probe_stamp.sh index e408b4dd..f5107ae3 100644 --- a/tests/host/manifest/test_probe_stamp.sh +++ b/tests/host/manifest/test_probe_stamp.sh @@ -35,10 +35,12 @@ gen cp "$stamp" "$build/default.stamp" if grep -q '^WT_FP_NEG_PROBE=0$' "$stamp" && grep -q '^WT_SEAL_NEG_PROBE=0$' "$stamp" && - grep -q '^WT_MSP_OVF_PROBE=0$' "$stamp"; then st=0; else st=1; fi + grep -q '^WT_MSP_OVF_PROBE=0$' "$stamp" && + grep -q '^WT_BUSFAULT_NEG_PROBE=0$' "$stamp"; then st=0; else st=1; fi check $st "default stamp records every probe off" -for probe in WT_FP_NEG_PROBE WT_SEAL_NEG_PROBE WT_MSP_OVF_PROBE; do +for probe in WT_FP_NEG_PROBE WT_SEAL_NEG_PROBE WT_MSP_OVF_PROBE \ + WT_BUSFAULT_NEG_PROBE; do gen "$probe=1" if grep -q "^$probe=1\$" "$stamp" && ! cmp -s "$stamp" "$build/default.stamp"; then st=0; else st=1; fi diff --git a/tests/target/lib/scenario.sh b/tests/target/lib/scenario.sh index 6debebf6..9251b40c 100644 --- a/tests/target/lib/scenario.sh +++ b/tests/target/lib/scenario.sh @@ -49,6 +49,7 @@ scenario_secure_flags() { sealbootneg) echo "WT_SEAL_NEG_PROBE=3" ;; sealpivotneg) echo "WT_SEAL_NEG_PROBE=4" ;; mspovfneg) echo "WT_MSP_OVF_PROBE=1" ;; + busfaultneg) echo "WT_BUSFAULT_NEG_PROBE=1" ;; *) echo "" ;; esac } diff --git a/tests/target/run_h5_hardware.sh b/tests/target/run_h5_hardware.sh index af86382c..34df8a14 100755 --- a/tests/target/run_h5_hardware.sh +++ b/tests/target/run_h5_hardware.sh @@ -42,7 +42,7 @@ set -o pipefail mode="${1:-all}" scenario="${2:-positive}" case "$mode" in build|flash|all) ;; *) echo "usage: $0 build|flash|all [scenario]" >&2; exit 2 ;; esac -case "$scenario" in positive|restart|crossdomain|keystoreneg|panicneg|confboot|devstorage|devcrypto|devattest|devattestqcbor|vaultrecover|vaultrecoversec|authneg|writeonce|hsmattackneg|bootupdate|vnet|vnetneg|gtzcneg|fpneg|sealneg|sealpivotneg|periphneg|mspovfneg) ;; *) echo "usage: $0 $mode positive|restart|crossdomain|keystoreneg|panicneg|confboot|devstorage|devcrypto|devattest|devattestqcbor|vaultrecover|vaultrecoversec|authneg|writeonce|hsmattackneg|bootupdate|vnet|vnetneg|gtzcneg|fpneg|sealneg|sealpivotneg|periphneg|mspovfneg" >&2; exit 2 ;; esac +case "$scenario" in positive|restart|crossdomain|keystoreneg|panicneg|confboot|devstorage|devcrypto|devattest|devattestqcbor|vaultrecover|vaultrecoversec|authneg|writeonce|hsmattackneg|bootupdate|vnet|vnetneg|gtzcneg|fpneg|sealneg|sealpivotneg|periphneg|mspovfneg|busfaultneg) ;; *) echo "usage: $0 $mode positive|restart|crossdomain|keystoreneg|panicneg|confboot|devstorage|devcrypto|devattest|devattestqcbor|vaultrecover|vaultrecoversec|authneg|writeonce|hsmattackneg|bootupdate|vnet|vnetneg|gtzcneg|fpneg|sealneg|sealpivotneg|periphneg|mspovfneg|busfaultneg" >&2; exit 2 ;; esac repo="$(cd "$(dirname "$0")/../.." && pwd)" cd "$repo" @@ -172,6 +172,7 @@ if [ "$mode" != "flash" ]; then [ "$scenario" = "sealneg" ] && secure_flags="WT_SEAL_NEG_PROBE=1" [ "$scenario" = "sealpivotneg" ] && secure_flags="WT_SEAL_NEG_PROBE=4" [ "$scenario" = "mspovfneg" ] && secure_flags="WT_MSP_OVF_PROBE=1" + [ "$scenario" = "busfaultneg" ] && secure_flags="WT_BUSFAULT_NEG_PROBE=1" [ "$scenario" = "vnet" ] && secure_flags="CONFIG_VNET=y" [ "$scenario" = "vnetneg" ] && secure_flags="CONFIG_VNET=y WT_VNET_NEG_PROBE=1" # WT_CONF_DIAG_TRAP=0: the emulator-only hang-probe fault would become a @@ -379,7 +380,7 @@ if [ "$mode" != "build" ]; then erase_verified 0x0C1FE000 erase_verified 0x0C1FA000 pyocd cmd -t "$PYOCD_TARGET" -c reset >/dev/null 2>&1 || true - elif [ "$scenario" = "positive" ] || [ "$scenario" = "bothpsa" ] || [ "$scenario" = "crossdomain" ] || [ "$scenario" = "keystoreneg" ] || [ "$scenario" = "panicneg" ] || [ "$scenario" = "fpneg" ] || [ "$scenario" = "sealneg" ] || [ "$scenario" = "sealpivotneg" ]; then + elif [ "$scenario" = "positive" ] || [ "$scenario" = "bothpsa" ] || [ "$scenario" = "crossdomain" ] || [ "$scenario" = "keystoreneg" ] || [ "$scenario" = "panicneg" ] || [ "$scenario" = "fpneg" ] || [ "$scenario" = "sealneg" ] || [ "$scenario" = "sealpivotneg" ] || [ "$scenario" = "busfaultneg" ]; then # Guest0's ITS+PS lifecycle persists vault objects across runs on silicon # (the emulator starts on fresh flash); blank the vault like the dev # scenarios do so the pool stays emulator-equivalent. @@ -882,6 +883,40 @@ if [ "$mode" != "build" ]; then fi expect "guest1 alive through the FP fault" "freertos_guest1: heartbeat" ;; + busfaultneg) + # The SERVICE_HSM relay SP reads the port's bus-error window (past the + # end of physical SRAM, MPU-permitted) on its first entry: a precise + # BusFault with BFAR, attributed to the partition, which restarts and + # then serves the full guest lifecycle; the platform never halts. + refute_re "BusFault did not escalate to HardFault" \ + '^(\[HARDFLT\]|HardFault|SecureFault)' + fault_cnt=$(read_secure_u32 g_tasklet_fault_count) + fault_cfsr=$(read_secure_u32 g_tasklet_fault_cfsr) + fault_addr=$(read_secure_u32 g_last_fault_address) + if [ -n "$fault_cnt" ] && [ $((0x$fault_cnt)) -ge 1 ]; then + check_pass "relay SP took the contained fault (count=0x$fault_cnt)" + else + check_fail "partition fault" "SP fault count not captured (count=${fault_cnt:-none})" + fi + if [ -n "$fault_cfsr" ] && \ + [ $(( (0x$fault_cfsr >> 8) & 0x82 )) -eq $((0x82)) ]; then + check_pass "fault was a precise BusFault with BFAR (CFSR=0x$fault_cfsr)" + else + check_fail "BusFault" "CFSR 0x${fault_cfsr:-none} lacks PRECISERR|BFARVALID" + fi + if [ -n "$fault_addr" ] && [ $((0x$fault_addr)) -eq $((0x300A0000)) ]; then + check_pass "BFAR names the probe's bus-error window (0x$fault_addr)" + else + check_fail "BFAR" "fault addr 0x${fault_addr:-none}, want 0x300A0000" + fi + lc=$(read_guest0_u32 g_guest0_lifecycle) + if [ -n "$lc" ] && [ $((0x$lc & 0xFF)) -eq 255 ]; then + check_pass "lifecycle completed after the partition restarted (0x$lc)" + else + check_fail "recovery" "lifecycle 0x${lc:-none} after the BusFault, expected 0xFF" + fi + expect "guest1 alive after the partition fault" "freertos_guest1: heartbeat" + ;; mspovfneg) # The reset path pushes on the main stack until MSPLIM_S raises STKOF; # the stackless halt latches the CFSR (bit 20) before the production From b3e06cba6fcf3815b4ca089f20158398aa94d21e Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 1 Oct 2026 10:49:37 -0700 Subject: [PATCH 5/9] Deny privileged execution from SPM RAM under a partition thread domain --- mk/arch-armv8m.mk | 4 ++ mk/common.mk | 1 + src/arch/armv8m/mpu_armv8m.c | 72 ++++++++++++++++++++++++- src/arch/armv8m/spm_svc.c | 27 ++++++++++ tests/host/manifest/test_probe_stamp.sh | 5 +- tests/target/lib/scenario.sh | 12 ++++- tests/target/lib/scenario_matrix.py | 1 + tests/target/run_h5_hardware.sh | 32 ++++++++++- tests/target/run_m33mu_scenario.sh | 25 +++++++-- 9 files changed, 171 insertions(+), 8 deletions(-) diff --git a/mk/arch-armv8m.mk b/mk/arch-armv8m.mk index a6e9f690..be9cb27a 100644 --- a/mk/arch-armv8m.mk +++ b/mk/arch-armv8m.mk @@ -9,6 +9,7 @@ ARCH_CFLAGS := -mcmse -DWT_TARGET_BUILD=1 WT_FP_NEG_PROBE ?= 0 WT_SEAL_NEG_PROBE ?= 0 WT_MSP_OVF_PROBE ?= 0 +WT_XN_NEG_PROBE ?= 0 ARCH_FP_SCAN_FLAGS := ifeq ($(WT_FP_NEG_PROBE),1) ARCH_CFLAGS += -DWT_FP_NEG_PROBE=1 @@ -20,6 +21,9 @@ endif ifeq ($(WT_MSP_OVF_PROBE),1) ARCH_CFLAGS += -DWT_MSP_OVF_PROBE=1 endif +ifeq ($(WT_XN_NEG_PROBE),1) +ARCH_CFLAGS += -DWT_XN_NEG_PROBE=1 +endif WT_WOLFCRYPT_SP_ASM ?= 1 WT_WOLFCRYPT_ARMASM ?= 1 diff --git a/mk/common.mk b/mk/common.mk index 03dd72d5..e046d472 100644 --- a/mk/common.mk +++ b/mk/common.mk @@ -1436,6 +1436,7 @@ $(BUILD_MODE_STAMP): FORCE | $(BUILD_DIR) 'WT_SEAL_NEG_PROBE=$(WT_SEAL_NEG_PROBE)' \ 'WT_MSP_OVF_PROBE=$(WT_MSP_OVF_PROBE)' \ 'WT_BUSFAULT_NEG_PROBE=$(WT_BUSFAULT_NEG_PROBE)' \ + 'WT_XN_NEG_PROBE=$(WT_XN_NEG_PROBE)' \ 'WT_REMEASURE_PROBE=$(WT_REMEASURE_PROBE)' \ 'WT_BOOTUPDATE_PROBE=$(WT_BOOTUPDATE_PROBE)' \ 'WT_MAX_GUESTS=$(WT_MAX_GUESTS)' \ diff --git a/src/arch/armv8m/mpu_armv8m.c b/src/arch/armv8m/mpu_armv8m.c index 979c0963..248877b7 100644 --- a/src/arch/armv8m/mpu_armv8m.c +++ b/src/arch/armv8m/mpu_armv8m.c @@ -27,6 +27,7 @@ #include "wolftrust/arch/armv8m/armv8m.h" #include "wolftrust/arch/armv8m/core_regs.h" #include "wolftrust/arch/armv8m/mmio_map.h" +#include "wolftrust/platform.h" #include "wolftrust/types.h" #include @@ -42,6 +43,14 @@ static const wt_armv8m_mpu_region_t* g_spm_whitelist; static size_t g_spm_whitelist_count; +/* SPM-private RAM: .data, .bss and the main stack, never inside a domain. */ +extern uint32_t _sdata; +extern uint32_t _estack; + +/* Partition dispatches that found no free region for the privileged + * execute-never cover of SPM RAM; only a conformance image may leave it set. */ +volatile uint32_t g_wt_xn_denied __attribute__((used)); + /* Program one secure MPU region. base/limit are inclusive 32-byte-aligned * boundaries; `rbar_flags` carries XN/AP/SH, `rlar_flags` carries AttrIndx. */ static void wt_mpu_s_set_region(uint32_t rnr, uintptr_t base, @@ -138,10 +147,52 @@ static void wt_program_secure_partition_region(uint32_t rnr, uintptr_t base, wt_mpu_s_set_region(rnr, base, base + size - 1u, rbar_flags, rlar_flags); } +/* The cover starts at the boot-handoff scratch when the port places it below + * .data, so all writable SPM RAM up to the main stack top is covered. */ +static uintptr_t wt_mpu_s_spm_ram_base(void) +{ + size_t size = 0u; + uintptr_t base = (uintptr_t)wt_platform_boot_handoff_region(&size); + + if (base == 0u || size == 0u || base >= (uintptr_t)&_sdata) { + base = (uintptr_t)&_sdata; + } + return base; +} + +/* With PRIVDEFENA the privileged handlers that run under a partition thread + * domain see the default map, which permits execution from SRAM; cover the + * SPM's own RAM privileged-only and execute-never so they cannot. */ +static void wt_mpu_s_cover_spm_ram(uint32_t rnr) +{ + wt_mpu_s_set_region(rnr, wt_mpu_s_spm_ram_base(), (uintptr_t)&_estack - 1u, + WT_MPU_RBAR_XN | WT_MPU_RBAR_AP_RW | + WT_MPU_RBAR_SH_INNER, WT_MPU_RLAR_ATTRIDX_NORMAL); +} + +/* A domain region inside SPM-private RAM breaks the level 3 invariant and + * would overlap the cover region; fail closed instead of dispatching. */ +static void wt_mpu_s_check_spm_ram_clear(const wt_memory_region_t* regions, + size_t count) +{ + size_t i; + uintptr_t base = wt_mpu_s_spm_ram_base(); + + for (i = 0u; regions != NULL && i < count; ++i) { + if (regions[i].size != 0u && + regions[i].base < (uintptr_t)&_estack && + regions[i].base + regions[i].size > base) { + wt_platform_panic(); + } + } +} + static void wt_program_sp_domain_regions(const wt_memory_region_t* regions, size_t count, uint32_t ctrl) { size_t i; + uint32_t next = WT_MAX_MEMORY_REGIONS; + uint32_t dregion = (WT_MPU_S_TYPE >> 8) & 0xFFu; WT_MPU_S_CTRL = 0u; wt_dsb(); @@ -160,7 +211,26 @@ static void wt_program_sp_domain_regions(const wt_memory_region_t* regions, WT_MPU_S_RLAR = 0u; } } - wt_mpu_s_disable_from(WT_MAX_MEMORY_REGIONS); + if ((ctrl & WT_MPU_CTRL_PRIVDEFENA) != 0u) { + wt_mpu_s_check_spm_ram_clear(regions, count); + if (count < WT_MAX_MEMORY_REGIONS) { + next = (uint32_t)count; + } + if (next < dregion) { + wt_mpu_s_cover_spm_ram(next); + next++; + } + else { +#if defined(WT_CONFORMANCE) && (WT_CONFORMANCE == 1) + /* The Arm client partition's window grants fill an 8-region + * MPU; the test image records the uncovered dispatch. */ + g_wt_xn_denied++; +#else + wt_platform_panic(); +#endif + } + } + wt_mpu_s_disable_from(next); wt_dsb(); WT_MPU_S_CTRL = ctrl; diff --git a/src/arch/armv8m/spm_svc.c b/src/arch/armv8m/spm_svc.c index fd2034ce..8e391a7d 100644 --- a/src/arch/armv8m/spm_svc.c +++ b/src/arch/armv8m/spm_svc.c @@ -45,6 +45,30 @@ static void wt_spm_sp_panic_trap(void) __asm volatile("udf #0x50"); } +#if defined(WT_XN_NEG_PROBE) && (WT_XN_NEG_PROBE == 1) +static uint16_t g_wt_xn_probe_thunk[2] __attribute__((aligned(4))); +static uint8_t g_wt_xn_probe_fired; + +/* xnneg: call a `bx lr` thunk placed in SPM .bss from the privileged gate, + * under the caller's thread domain; the execute-never cover must fault it + * (IACCVIOL) before it returns, or the udf marks the escape. */ +static void wt_spm_xn_probe(void) +{ + void (*thunk)(void); + + if (g_wt_xn_probe_fired != 0u) { + return; + } + g_wt_xn_probe_fired = 1u; + g_wt_xn_probe_thunk[0] = 0x4770u; + g_wt_xn_probe_thunk[1] = 0xBF00u; + __asm volatile("dsb 0xF\n isb 0xF" ::: "memory"); + thunk = (void (*)(void))((uintptr_t)g_wt_xn_probe_thunk | 1u); + thunk(); + __asm volatile("udf #0x52"); +} +#endif + /* Tail-called from SVC_Handler with r0 = the exception frame; the stacked r0 * carries the call block in and the gate-level status out. */ __attribute__((used)) @@ -52,6 +76,9 @@ void wt_spm_svc_entry(uint32_t* frame) { wt_spm_call_t* call = (wt_spm_call_t*)(uintptr_t)frame[0]; +#if defined(WT_XN_NEG_PROBE) && (WT_XN_NEG_PROBE == 1) + wt_spm_xn_probe(); +#endif frame[0] = (uint32_t)wt_spm_dispatch_call(call, (wt_trap_frame_t*)frame); } diff --git a/tests/host/manifest/test_probe_stamp.sh b/tests/host/manifest/test_probe_stamp.sh index f5107ae3..1e537416 100644 --- a/tests/host/manifest/test_probe_stamp.sh +++ b/tests/host/manifest/test_probe_stamp.sh @@ -36,11 +36,12 @@ cp "$stamp" "$build/default.stamp" if grep -q '^WT_FP_NEG_PROBE=0$' "$stamp" && grep -q '^WT_SEAL_NEG_PROBE=0$' "$stamp" && grep -q '^WT_MSP_OVF_PROBE=0$' "$stamp" && - grep -q '^WT_BUSFAULT_NEG_PROBE=0$' "$stamp"; then st=0; else st=1; fi + grep -q '^WT_BUSFAULT_NEG_PROBE=0$' "$stamp" && + grep -q '^WT_XN_NEG_PROBE=0$' "$stamp"; then st=0; else st=1; fi check $st "default stamp records every probe off" for probe in WT_FP_NEG_PROBE WT_SEAL_NEG_PROBE WT_MSP_OVF_PROBE \ - WT_BUSFAULT_NEG_PROBE; do + WT_BUSFAULT_NEG_PROBE WT_XN_NEG_PROBE; do gen "$probe=1" if grep -q "^$probe=1\$" "$stamp" && ! cmp -s "$stamp" "$build/default.stamp"; then st=0; else st=1; fi diff --git a/tests/target/lib/scenario.sh b/tests/target/lib/scenario.sh index 9251b40c..d337a08e 100644 --- a/tests/target/lib/scenario.sh +++ b/tests/target/lib/scenario.sh @@ -50,6 +50,7 @@ scenario_secure_flags() { sealpivotneg) echo "WT_SEAL_NEG_PROBE=4" ;; mspovfneg) echo "WT_MSP_OVF_PROBE=1" ;; busfaultneg) echo "WT_BUSFAULT_NEG_PROBE=1" ;; + xnneg) echo "WT_XN_NEG_PROBE=1" ;; *) echo "" ;; esac } @@ -59,7 +60,7 @@ scenario_secure_flags() { scenario_end() { case "$1" in rollbackneg|spbudgetneg) echo "bkpt:0x7d" ;; - manifestneg|manifestneg2|sealbootneg|mspovfneg) echo "bkpt:0x7e" ;; + manifestneg|manifestneg2|sealbootneg|mspovfneg|xnneg) echo "bkpt:0x7e" ;; sealhaltneg) echo "bkpt:0x6e" ;; remeasureneg) echo "bkpt:0x6c" ;; *) echo "idle" ;; @@ -103,6 +104,15 @@ scenario_assert_verdict() { refute_re "the mandatory service never completed a guest lifecycle" \ "$GUEST_DONE_RE" ;; + xnneg) + expect_re "privileged execution from SPM RAM faulted (IACCVIOL)" \ + '\[MEMFAULT\] pc=0x30[0-9a-f]{6} addr=0x30[0-9a-f]{6}' + refute_re "the thunk never returned into the gate" '\[USGFLT\]' + expect "SPM-origin fault halted the platform fail-closed" \ + "[BKPT] imm=0x7e" + refute_re "no guest scheduled after the halt" \ + "$GUEST_STARTED_RE" + ;; mspovfneg) expect_re "main-stack overflow raised STKOF against MSPLIM_S" \ '\[USGFLT\].*CFSR=0x00[1-9a-f][0-9a-f]0000' diff --git a/tests/target/lib/scenario_matrix.py b/tests/target/lib/scenario_matrix.py index cc1b3fda..1f29a23b 100755 --- a/tests/target/lib/scenario_matrix.py +++ b/tests/target/lib/scenario_matrix.py @@ -92,6 +92,7 @@ ("sealbootneg", "Damaged main-stack seal refuses to boot"), ("sealpivotneg", "Blocking wait stacked on the seal stays contained"), ("mspovfneg", "SPM main-stack overflow halts fail-closed"), + ("xnneg", "Privileged execution from SPM RAM denied"), ), }, "mimxrt700": { diff --git a/tests/target/run_h5_hardware.sh b/tests/target/run_h5_hardware.sh index 34df8a14..7cc7a2f8 100755 --- a/tests/target/run_h5_hardware.sh +++ b/tests/target/run_h5_hardware.sh @@ -42,7 +42,7 @@ set -o pipefail mode="${1:-all}" scenario="${2:-positive}" case "$mode" in build|flash|all) ;; *) echo "usage: $0 build|flash|all [scenario]" >&2; exit 2 ;; esac -case "$scenario" in positive|restart|crossdomain|keystoreneg|panicneg|confboot|devstorage|devcrypto|devattest|devattestqcbor|vaultrecover|vaultrecoversec|authneg|writeonce|hsmattackneg|bootupdate|vnet|vnetneg|gtzcneg|fpneg|sealneg|sealpivotneg|periphneg|mspovfneg|busfaultneg) ;; *) echo "usage: $0 $mode positive|restart|crossdomain|keystoreneg|panicneg|confboot|devstorage|devcrypto|devattest|devattestqcbor|vaultrecover|vaultrecoversec|authneg|writeonce|hsmattackneg|bootupdate|vnet|vnetneg|gtzcneg|fpneg|sealneg|sealpivotneg|periphneg|mspovfneg|busfaultneg" >&2; exit 2 ;; esac +case "$scenario" in positive|restart|crossdomain|keystoreneg|panicneg|confboot|devstorage|devcrypto|devattest|devattestqcbor|vaultrecover|vaultrecoversec|authneg|writeonce|hsmattackneg|bootupdate|vnet|vnetneg|gtzcneg|fpneg|sealneg|sealpivotneg|periphneg|mspovfneg|busfaultneg|xnneg) ;; *) echo "usage: $0 $mode positive|restart|crossdomain|keystoreneg|panicneg|confboot|devstorage|devcrypto|devattest|devattestqcbor|vaultrecover|vaultrecoversec|authneg|writeonce|hsmattackneg|bootupdate|vnet|vnetneg|gtzcneg|fpneg|sealneg|sealpivotneg|periphneg|mspovfneg|busfaultneg|xnneg" >&2; exit 2 ;; esac repo="$(cd "$(dirname "$0")/../.." && pwd)" cd "$repo" @@ -173,6 +173,7 @@ if [ "$mode" != "flash" ]; then [ "$scenario" = "sealpivotneg" ] && secure_flags="WT_SEAL_NEG_PROBE=4" [ "$scenario" = "mspovfneg" ] && secure_flags="WT_MSP_OVF_PROBE=1" [ "$scenario" = "busfaultneg" ] && secure_flags="WT_BUSFAULT_NEG_PROBE=1" + [ "$scenario" = "xnneg" ] && secure_flags="WT_XN_NEG_PROBE=1" [ "$scenario" = "vnet" ] && secure_flags="CONFIG_VNET=y" [ "$scenario" = "vnetneg" ] && secure_flags="CONFIG_VNET=y WT_VNET_NEG_PROBE=1" # WT_CONF_DIAG_TRAP=0: the emulator-only hang-probe fault would become a @@ -517,6 +518,14 @@ if [ "$mode" != "build" ]; then else check_fail "cross-guest isolation" "guest1 negative mask 0x${neg:-none}, want bit 0x4" fi + # Every production partition dispatch carried the privileged + # execute-never cover of SPM RAM (12 MPU regions on the H563). + xn=$(read_secure_u32 g_wt_xn_denied) + if [ -n "$xn" ] && [ $((0x$xn)) -eq 0 ]; then + check_pass "SPM RAM execute-never cover present on every dispatch" + else + check_fail "XN cover" "g_wt_xn_denied=0x${xn:-none}, want 0" + fi ;; authneg) # Authenticated launch fails closed (WT-SYS-0002): guest0's flashed image @@ -917,6 +926,27 @@ if [ "$mode" != "build" ]; then fi expect "guest1 alive after the partition fault" "freertos_guest1: heartbeat" ;; + xnneg) + # The privileged SVC gate calls a thunk in SPM .bss under a partition + # thread domain; the execute-never cover faults the fetch (IACCVIOL, + # escalated from the gate's priority to HardFault) and the SPM latch + # names the thunk address before the production panic. No guest runs. + cfsr=$(read_secure_u32 g_wt_spm_fault_cfsr) + pc=$(read_secure_u32 g_wt_spm_fault_pc) + if [ -n "$cfsr" ] && [ $((0x$cfsr & 0x1)) -eq 1 ]; then + check_pass "privileged execution from SPM RAM faulted (IACCVIOL, CFSR=0x$cfsr)" + else + check_fail "XN" "CFSR 0x${cfsr:-none} lacks IACCVIOL" + fi + if [ -n "$pc" ] && [ $((0x$pc)) -ge $((0x30028000)) ] && \ + [ $((0x$pc)) -lt $((0x30075000)) ]; then + check_pass "faulting fetch was inside SPM RAM (pc=0x$pc)" + else + check_fail "XN" "fault pc 0x${pc:-none} not in SPM RAM" + fi + refute_re "no guest ran after the halt" \ + '(guest0_psa alive|freertos_guest1: heartbeat)' + ;; mspovfneg) # The reset path pushes on the main stack until MSPLIM_S raises STKOF; # the stackless halt latches the CFSR (bit 20) before the production diff --git a/tests/target/run_m33mu_scenario.sh b/tests/target/run_m33mu_scenario.sh index 351a832d..f3aa1cdf 100755 --- a/tests/target/run_m33mu_scenario.sh +++ b/tests/target/run_m33mu_scenario.sh @@ -296,9 +296,10 @@ elif [ "$scenario" = "sealbootneg" ]; then # The reset path refuses the damaged main-stack seal before any partition. expect_bkpt=0x7e timeout_s=40 -elif [ "$scenario" = "mspovfneg" ]; then - # The reset path overflows the main stack on purpose; MSPLIM_S raises STKOF - # and the SPM halts on the production panic. Do not quit on the fault. +elif [ "$scenario" = "mspovfneg" ] || [ "$scenario" = "xnneg" ]; then + # The SPM faults itself on purpose (main-stack overflow against MSPLIM_S, + # or privileged execution from its own RAM) and halts on the production + # panic. Do not quit on the fault. quit_flag="" expect_bkpt=0x7e timeout_s=40 @@ -1010,6 +1011,24 @@ case "$scenario" in expect "run reached the clean scenario end" "[EXPECT BKPT] Success" echo "PASS: target/sealpivotneg" ;; + xnneg) + # The privileged SVC gate, running under a partition's thread domain, + # calls a `bx lr` thunk copied into SPM .bss. The execute-never cover of + # SPM RAM must fault the fetch (IACCVIOL at the thunk address) and the + # SPM-origin fault halts the platform; a thunk that returned would run + # the probe's udf #0x52 instead, a UsageFault the checks refuse. + if grep -Eq '\[MEMFAULT\] pc=0x30[0-9a-f]{6} addr=0x30[0-9a-f]{6}' "$log"; then + check_pass "privileged execution from SPM RAM faulted (IACCVIOL)" + else + check_fail "XN" "expected an instruction-fetch MemManage in SPM RAM, none seen" + fi + refute_re "the thunk never returned into the gate" '\[USGFLT\]' + expect "SPM-origin fault halted the platform fail-closed" "[BKPT] imm=0x7e" + expect "the emulator stopped on that halt" "[EXPECT BKPT] Success" + refute_re "no guest ran after the halt" \ + '(guest0_psa alive|freertos_guest1:|vnet-guest|\[BKPT\] imm=0x7f)' + echo "PASS: target/xnneg" + ;; mspovfneg) # The SPM pushes on its own stack until MSPLIM_S raises STKOF (CFSR bit # 20, no partition frame to blame); the stackless halt must land on the From e9de7a9cc1860617df0d538d0c33a5ebfd60edb7 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 1 Oct 2026 10:51:43 -0700 Subject: [PATCH 6/9] Panic only the partition that issues the scheduler's guest-return SVC --- include/wolftrust/arch.h | 4 ++++ mk/common.mk | 5 +++++ src/arch/armv8m/coroutine_armv8m.c | 5 +++-- src/arch/armv8m/spm_svc.c | 15 +++++++++++++ src/arch/common/spm_gate_core.c | 4 +++- src/spm_partitions.c | 19 ++++++++++------- tests/host/manifest/test_probe_stamp.sh | 5 +++-- tests/target/lib/scenario.sh | 1 + tests/target/lib/scenario_matrix.py | 1 + tests/target/run_h5_hardware.sh | 10 +++++---- tests/target/run_m33mu_scenario.sh | 28 ++++++++++++++++++++++++- 11 files changed, 80 insertions(+), 17 deletions(-) diff --git a/include/wolftrust/arch.h b/include/wolftrust/arch.h index d76ce656..62f52bb4 100644 --- a/include/wolftrust/arch.h +++ b/include/wolftrust/arch.h @@ -87,6 +87,10 @@ uintptr_t wt_arch_sp_stack_pointer(void); void wt_arch_sp_redirect_to_panic_trap(wt_trap_frame_t* frame); void wt_arch_assert_privileged_thread(void); void wt_arch_sp_fault_probe(unsigned int code); +#if defined(WT_SVC_NEG_PROBE) && (WT_SVC_NEG_PROBE == 1) +/* svcneg: issue the scheduler's internal guest-return trap from a partition. */ +void wt_arch_sp_guest_return_probe(void); +#endif void wt_arch_diag_trap(uint32_t a, uint32_t b, uint32_t c); void wt_arch_sp_panic(uint32_t op, uint32_t code, uint32_t extra) __attribute__((noreturn)); diff --git a/mk/common.mk b/mk/common.mk index e046d472..5146842c 100644 --- a/mk/common.mk +++ b/mk/common.mk @@ -87,6 +87,7 @@ WT_HSM_FAULT_PROBE ?= 0 WT_SP_FAULT_ALWAYS_PROBE ?= 0 WT_PANIC_NEG_PROBE ?= 0 WT_BUSFAULT_NEG_PROBE ?= 0 +WT_SVC_NEG_PROBE ?= 0 WT_VNET_NEG_PROBE ?= 0 WT_MANIFEST_NEG_PROBE ?= 0 WT_REMEASURE_PROBE ?= 0 @@ -198,6 +199,9 @@ endif ifeq ($(WT_BUSFAULT_NEG_PROBE),1) SECURE_CFLAGS += -DWT_BUSFAULT_NEG_PROBE=1 endif +ifeq ($(WT_SVC_NEG_PROBE),1) +SECURE_CFLAGS += -DWT_SVC_NEG_PROBE=1 +endif ifeq ($(WT_VNET_NEG_PROBE),1) SECURE_CFLAGS += -DWT_VNET_NEG_PROBE=1 endif @@ -1437,6 +1441,7 @@ $(BUILD_MODE_STAMP): FORCE | $(BUILD_DIR) 'WT_MSP_OVF_PROBE=$(WT_MSP_OVF_PROBE)' \ 'WT_BUSFAULT_NEG_PROBE=$(WT_BUSFAULT_NEG_PROBE)' \ 'WT_XN_NEG_PROBE=$(WT_XN_NEG_PROBE)' \ + 'WT_SVC_NEG_PROBE=$(WT_SVC_NEG_PROBE)' \ 'WT_REMEASURE_PROBE=$(WT_REMEASURE_PROBE)' \ 'WT_BOOTUPDATE_PROBE=$(WT_BOOTUPDATE_PROBE)' \ 'WT_MAX_GUESTS=$(WT_MAX_GUESTS)' \ diff --git a/src/arch/armv8m/coroutine_armv8m.c b/src/arch/armv8m/coroutine_armv8m.c index 44b05d5f..c6188909 100644 --- a/src/arch/armv8m/coroutine_armv8m.c +++ b/src/arch/armv8m/coroutine_armv8m.c @@ -295,10 +295,11 @@ void SVC_Handler(void) "bne 2f \n" /* Internal guest-return is privileged MSP-thread-only: a PSP-origin * caller is a Secure Partition attempting the scheduler's own SVC, - * which fails the platform closed instead of restoring SPM state. */ + * a PROGRAMMER ERROR that panics that partition alone. */ "tst lr, #4 \n" "beq wt_armv8m_svc_guest_return \n" - "b wt_platform_panic \n" + "mov r0, r2 \n" + "b wt_spm_svc_reject \n" "2: \n" "cmp r3, #0x01 \n" "bne 1f \n" diff --git a/src/arch/armv8m/spm_svc.c b/src/arch/armv8m/spm_svc.c index 8e391a7d..c85bf89e 100644 --- a/src/arch/armv8m/spm_svc.c +++ b/src/arch/armv8m/spm_svc.c @@ -82,6 +82,21 @@ void wt_spm_svc_entry(uint32_t* frame) frame[0] = (uint32_t)wt_spm_dispatch_call(call, (wt_trap_frame_t*)frame); } +/* A partition issued the scheduler's own SVC: resume it on the panic trap so + * it is quarantined under its policy instead of halting the platform. */ +__attribute__((used)) +void wt_spm_svc_reject(uint32_t* frame) +{ + wt_arch_sp_redirect_to_panic_trap((wt_trap_frame_t*)frame); +} + +#if defined(WT_SVC_NEG_PROBE) && (WT_SVC_NEG_PROBE == 1) +void wt_arch_sp_guest_return_probe(void) +{ + __asm volatile("svc #0x7F"); +} +#endif + uintptr_t wt_arch_sp_stack_pointer(void) { uint32_t psp; diff --git a/src/arch/common/spm_gate_core.c b/src/arch/common/spm_gate_core.c index 90f9bc09..4fab3fd1 100644 --- a/src/arch/common/spm_gate_core.c +++ b/src/arch/common/spm_gate_core.c @@ -256,7 +256,9 @@ static int wt_spm_fault_restart(void* ctx) void* arg = slot->arg; #if (defined(WT_SP_FAULT_PROBE) && (WT_SP_FAULT_PROBE == 1)) || \ - (defined(WT_PANIC_NEG_PROBE) && (WT_PANIC_NEG_PROBE == 1)) + (defined(WT_PANIC_NEG_PROBE) && (WT_PANIC_NEG_PROBE == 1)) || \ + (defined(WT_BUSFAULT_NEG_PROBE) && (WT_BUSFAULT_NEG_PROBE == 1)) || \ + (defined(WT_SVC_NEG_PROBE) && (WT_SVC_NEG_PROBE == 1)) arg = (void*)((intptr_t)slot->arg | WT_SP_FAULT_PROBE_RESTARTED); #endif #if defined(WT_VNET_NEG_PROBE) && (WT_VNET_NEG_PROBE == 1) diff --git a/src/spm_partitions.c b/src/spm_partitions.c index 5b8a88b9..f1954fab 100644 --- a/src/spm_partitions.c +++ b/src/spm_partitions.c @@ -185,22 +185,27 @@ static void wt_spm_its_entry(void* arg) volatile uint32_t periph_probe; #endif -#if defined(WT_PANIC_NEG_PROBE) && (WT_PANIC_NEG_PROBE == 1) - /* Secure-caller-misuse proof (target/panicneg): closing an error-status - * handle is an FF-M PROGRAMMER ERROR the production SPM must panic this - * partition for; the graceful recovery restarts it with the marker set - * and the re-run serves storage normally. Reaching the udf below means - * the SPM failed to panic the caller, which fails the scenario with a - * distinct fault. Never built into production images. */ +#if (defined(WT_PANIC_NEG_PROBE) && (WT_PANIC_NEG_PROBE == 1)) || \ + (defined(WT_SVC_NEG_PROBE) && (WT_SVC_NEG_PROBE == 1)) + /* Secure-caller-misuse proofs (target/panicneg, svcneg): closing an + * error-status handle, or issuing the scheduler's internal SVC, is an + * FF-M PROGRAMMER ERROR the production SPM must panic this partition + * for; the graceful recovery restarts it with the marker set and the + * re-run serves storage normally. Reaching the udf below means the SPM + * failed to panic the caller. Never built into production images. */ partition_id = (int32_t)((intptr_t)arg & ~(intptr_t)WT_SP_FAULT_PROBE_RESTARTED); if (((intptr_t)arg & WT_SP_FAULT_PROBE_RESTARTED) == 0) { +#if defined(WT_SVC_NEG_PROBE) && (WT_SVC_NEG_PROBE == 1) + wt_arch_sp_guest_return_probe(); +#else wt_spm_call_t bad; (void)memset(&bad, 0, sizeof(bad)); bad.op = WT_SPM_OP_CLOSE; bad.msg_handle = (psa_handle_t)-135; (void)wt_arch_sp_trap(&bad); +#endif wt_arch_sp_fault_probe(3u); } #endif diff --git a/tests/host/manifest/test_probe_stamp.sh b/tests/host/manifest/test_probe_stamp.sh index 1e537416..8f90fad2 100644 --- a/tests/host/manifest/test_probe_stamp.sh +++ b/tests/host/manifest/test_probe_stamp.sh @@ -37,11 +37,12 @@ if grep -q '^WT_FP_NEG_PROBE=0$' "$stamp" && grep -q '^WT_SEAL_NEG_PROBE=0$' "$stamp" && grep -q '^WT_MSP_OVF_PROBE=0$' "$stamp" && grep -q '^WT_BUSFAULT_NEG_PROBE=0$' "$stamp" && - grep -q '^WT_XN_NEG_PROBE=0$' "$stamp"; then st=0; else st=1; fi + grep -q '^WT_XN_NEG_PROBE=0$' "$stamp" && + grep -q '^WT_SVC_NEG_PROBE=0$' "$stamp"; then st=0; else st=1; fi check $st "default stamp records every probe off" for probe in WT_FP_NEG_PROBE WT_SEAL_NEG_PROBE WT_MSP_OVF_PROBE \ - WT_BUSFAULT_NEG_PROBE WT_XN_NEG_PROBE; do + WT_BUSFAULT_NEG_PROBE WT_XN_NEG_PROBE WT_SVC_NEG_PROBE; do gen "$probe=1" if grep -q "^$probe=1\$" "$stamp" && ! cmp -s "$stamp" "$build/default.stamp"; then st=0; else st=1; fi diff --git a/tests/target/lib/scenario.sh b/tests/target/lib/scenario.sh index d337a08e..5276dd3a 100644 --- a/tests/target/lib/scenario.sh +++ b/tests/target/lib/scenario.sh @@ -51,6 +51,7 @@ scenario_secure_flags() { mspovfneg) echo "WT_MSP_OVF_PROBE=1" ;; busfaultneg) echo "WT_BUSFAULT_NEG_PROBE=1" ;; xnneg) echo "WT_XN_NEG_PROBE=1" ;; + svcneg) echo "WT_SVC_NEG_PROBE=1" ;; *) echo "" ;; esac } diff --git a/tests/target/lib/scenario_matrix.py b/tests/target/lib/scenario_matrix.py index 1f29a23b..0ed2aa2c 100755 --- a/tests/target/lib/scenario_matrix.py +++ b/tests/target/lib/scenario_matrix.py @@ -93,6 +93,7 @@ ("sealpivotneg", "Blocking wait stacked on the seal stays contained"), ("mspovfneg", "SPM main-stack overflow halts fail-closed"), ("xnneg", "Privileged execution from SPM RAM denied"), + ("svcneg", "Partition guest-return SVC panics only that partition"), ), }, "mimxrt700": { diff --git a/tests/target/run_h5_hardware.sh b/tests/target/run_h5_hardware.sh index 7cc7a2f8..5dab2ddf 100755 --- a/tests/target/run_h5_hardware.sh +++ b/tests/target/run_h5_hardware.sh @@ -42,7 +42,7 @@ set -o pipefail mode="${1:-all}" scenario="${2:-positive}" case "$mode" in build|flash|all) ;; *) echo "usage: $0 build|flash|all [scenario]" >&2; exit 2 ;; esac -case "$scenario" in positive|restart|crossdomain|keystoreneg|panicneg|confboot|devstorage|devcrypto|devattest|devattestqcbor|vaultrecover|vaultrecoversec|authneg|writeonce|hsmattackneg|bootupdate|vnet|vnetneg|gtzcneg|fpneg|sealneg|sealpivotneg|periphneg|mspovfneg|busfaultneg|xnneg) ;; *) echo "usage: $0 $mode positive|restart|crossdomain|keystoreneg|panicneg|confboot|devstorage|devcrypto|devattest|devattestqcbor|vaultrecover|vaultrecoversec|authneg|writeonce|hsmattackneg|bootupdate|vnet|vnetneg|gtzcneg|fpneg|sealneg|sealpivotneg|periphneg|mspovfneg|busfaultneg|xnneg" >&2; exit 2 ;; esac +case "$scenario" in positive|restart|crossdomain|keystoreneg|panicneg|confboot|devstorage|devcrypto|devattest|devattestqcbor|vaultrecover|vaultrecoversec|authneg|writeonce|hsmattackneg|bootupdate|vnet|vnetneg|gtzcneg|fpneg|sealneg|sealpivotneg|periphneg|mspovfneg|busfaultneg|xnneg|svcneg) ;; *) echo "usage: $0 $mode positive|restart|crossdomain|keystoreneg|panicneg|confboot|devstorage|devcrypto|devattest|devattestqcbor|vaultrecover|vaultrecoversec|authneg|writeonce|hsmattackneg|bootupdate|vnet|vnetneg|gtzcneg|fpneg|sealneg|sealpivotneg|periphneg|mspovfneg|busfaultneg|xnneg|svcneg" >&2; exit 2 ;; esac repo="$(cd "$(dirname "$0")/../.." && pwd)" cd "$repo" @@ -174,6 +174,7 @@ if [ "$mode" != "flash" ]; then [ "$scenario" = "mspovfneg" ] && secure_flags="WT_MSP_OVF_PROBE=1" [ "$scenario" = "busfaultneg" ] && secure_flags="WT_BUSFAULT_NEG_PROBE=1" [ "$scenario" = "xnneg" ] && secure_flags="WT_XN_NEG_PROBE=1" + [ "$scenario" = "svcneg" ] && secure_flags="WT_SVC_NEG_PROBE=1" [ "$scenario" = "vnet" ] && secure_flags="CONFIG_VNET=y" [ "$scenario" = "vnetneg" ] && secure_flags="CONFIG_VNET=y WT_VNET_NEG_PROBE=1" # WT_CONF_DIAG_TRAP=0: the emulator-only hang-probe fault would become a @@ -381,7 +382,7 @@ if [ "$mode" != "build" ]; then erase_verified 0x0C1FE000 erase_verified 0x0C1FA000 pyocd cmd -t "$PYOCD_TARGET" -c reset >/dev/null 2>&1 || true - elif [ "$scenario" = "positive" ] || [ "$scenario" = "bothpsa" ] || [ "$scenario" = "crossdomain" ] || [ "$scenario" = "keystoreneg" ] || [ "$scenario" = "panicneg" ] || [ "$scenario" = "fpneg" ] || [ "$scenario" = "sealneg" ] || [ "$scenario" = "sealpivotneg" ] || [ "$scenario" = "busfaultneg" ]; then + elif [ "$scenario" = "positive" ] || [ "$scenario" = "bothpsa" ] || [ "$scenario" = "crossdomain" ] || [ "$scenario" = "keystoreneg" ] || [ "$scenario" = "panicneg" ] || [ "$scenario" = "fpneg" ] || [ "$scenario" = "sealneg" ] || [ "$scenario" = "sealpivotneg" ] || [ "$scenario" = "busfaultneg" ] || [ "$scenario" = "svcneg" ]; then # Guest0's ITS+PS lifecycle persists vault objects across runs on silicon # (the emulator starts on fresh flash); blank the vault like the dev # scenarios do so the pool stays emulator-equivalent. @@ -653,8 +654,9 @@ if [ "$mode" != "build" ]; then fi expect "guest1 alive after SP quarantined" "freertos_guest1: heartbeat" ;; - panicneg) - # Secure-caller misuse: the ITS SP closes an error-status handle on its + panicneg|svcneg) + # Secure-caller misuse: the ITS SP closes an error-status handle + # (panicneg) or issues the scheduler's internal SVC (svcneg) on its # first entry, so the production SPM panics it (resume PC landed on an # undefined instruction -> UsageFault UNDEFINSTR in the CFSR latch), the # graceful recovery restarts it, and the RESTARTED partition must then diff --git a/tests/target/run_m33mu_scenario.sh b/tests/target/run_m33mu_scenario.sh index f3aa1cdf..d82a6cff 100755 --- a/tests/target/run_m33mu_scenario.sh +++ b/tests/target/run_m33mu_scenario.sh @@ -305,7 +305,7 @@ elif [ "$scenario" = "mspovfneg" ] || [ "$scenario" = "xnneg" ]; then timeout_s=40 elif [ "$scenario" = "spfaultneg" ] || [ "$scenario" = "panicneg" ] || [ "$scenario" = "vnetneg" ] || [ "$scenario" = "sealneg" ] || - [ "$scenario" = "sealpivotneg" ]; then + [ "$scenario" = "sealpivotneg" ] || [ "$scenario" = "svcneg" ]; then # The SP faults on purpose; wolfTrust catches the fault and restarts # the partition in place, so halting on the fault would defeat the # recovery. The rest of the lifecycle then completes normally through the @@ -1011,6 +1011,32 @@ case "$scenario" in expect "run reached the clean scenario end" "[EXPECT BKPT] Success" echo "PASS: target/sealpivotneg" ;; + svcneg) + # The ITS SP issues the scheduler's internal guest-return SVC (0x7F) on + # its first entry. A PSP-origin caller is a PROGRAMMER ERROR: the SPM + # resumes that partition on the panic trap (udf #0x50 -> Secure-Thread + # UNDEFINSTR UsageFault), the pinned client is unblocked with + # COMMUNICATION_FAILURE, and the platform keeps running. A missed reject + # would halt the platform (BKPT 0x7E) or run the probe's udf #3. + if grep -Eq '\[USGFLT\].*CFSR=0x00010000' "$log" && + grep -Eq '\[USGFLT\] mem16\[0x[0-9a-f]+\]=0xde50' "$log"; then + check_pass "ITS SP panicked once on the panic trap (Secure-Thread UNDEFINSTR)" + else + check_fail "SP panic" "expected the panic-trap UsageFault, none seen" + fi + refute_re "panic was contained, not escalated" \ + '(\[HARDFLT\]|HardFault|SecureFault)' + refute_re "platform did not halt on the partition's SVC" \ + '\[BKPT\] imm=0x(6e|7e|7d)' + expect "pinned client unblocked with COMMUNICATION_FAILURE" \ + "psa_connect(SERVICE_ITS) failed rc=0 handle=-145" + expect "sealed storage path unaffected" \ + "wolfTrust PS sealed set/get verified" + expect "unrelated guest booted and ran through the panic" \ + "freertos_guest1: alive" + expect "full lifecycle completed after recovery" "[EXPECT BKPT] Success" + echo "PASS: target/svcneg" + ;; xnneg) # The privileged SVC gate, running under a partition's thread domain, # calls a `bx lr` thunk copied into SPM .bss. The execute-never cover of From 624c3ead7fca073de6822dccf448bf752814efb2 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 1 Oct 2026 10:53:20 -0700 Subject: [PATCH 7/9] Document the SPM stack limit, BusFault containment, and execute-never policy --- docs/Security-Model.md | 43 ++++++++++++++++++++++++++++++++++++++++++ docs/Testing.md | 17 +++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/docs/Security-Model.md b/docs/Security-Model.md index 00d52862..600d6a18 100644 --- a/docs/Security-Model.md +++ b/docs/Security-Model.md @@ -143,6 +143,47 @@ moved onto a seal while another context runs, so this check does not replace the integrity signature the processor places in, and checks on, the Secure frames it stacks itself. +### SPM stack limit and fault attribution + +The Secure main stack has a fixed size (`WT_SPM_STACK_SIZE`, 16 KiB), reserved +by the linker below `_estack`; the link fails if `.bss` reaches it. Reset sets +`MSPLIM_S` to its bottom right after MSP is placed under the seal words, and +boot refuses to continue unless the limit reads back. An SPM stack overflow +raises `UsageFault.STKOF` on the main stack; the handler halts the platform on +the production panic without touching the stack, and a real `HardFault` +handler does the same for any escalated fault, latching `CFSR`, `HFSR`, +`MMFAR`, `BFAR`, the stacked PC and `EXC_RETURN` for a debugger instead of +spinning silently. + +`BusFault` is enabled alongside `MemManage` and `UsageFault`. All three take one +dispatcher, which attributes the fault by the frame it finds: a Secure Thread +frame on the process stack with a scheduled partition or wolfHSM tasklet +current is that partition's fault (precise bus errors carry `BFAR`; an +imprecise one is drained by the barrier every context switch issues, so it is +still pending against the partition that issued the write), and the partition +restarts under its manifest policy. A frame from a privileged handler, the +bootstrap thread, or no current coroutine is the SPM's own fault and halts the +platform. A Non-secure bus error targets the Secure `BusFault` as well +(`BFHFNMINS` is 0); it is routed to the guest fault path and restarts the +guest. A partition that issues the scheduler's internal guest-return `SVC` is +resumed on the PROGRAMMER ERROR trap and restarts alone. + +### Execute-never Secure RAM + +The SPM whitelist maps every writable Secure RAM region execute-never. The one +executable Secure RAM window is the MIMXRT700's RAMFUNC band, which holds the +NSC gateway and NOR routines and is mapped read-only. While an +unprivileged partition thread runs, its MPU table keeps `PRIVDEFENA` so the +privileged SVC gate and its deputies can reach SPM state, and the default map +would let privileged code execute from SRAM; one extra region therefore covers +the SPM's own RAM (the boot-handoff scratch, `.data`, `.bss`, the main stack) +privileged-only and execute-never on every partition dispatch, and a domain +region inside that window fails the dispatch closed. A production partition +table must leave the region free; the Arm conformance client partition fills +an 8-region MPU with its window grants, and the conformance image counts those +dispatches in `g_wt_xn_denied` instead (the STM32H563 implements 12 regions, +so it is covered there). + ### Link-time optimization The Secure image enables GCC link-time optimization by default. LTO can replace @@ -270,6 +311,8 @@ engine images. - [FF-M gateway](../src/arch/armv8m/ffm_nsc.c) - [Secure Partition scheduler and SVC gates](../src/arch/armv8m/spm_svc.c) - [Secure stack sealing and context switch](../src/arch/armv8m/coroutine_armv8m.c) +- [Secure fault attribution and SPM halt](../src/arch/armv8m/sp_fault_armv8m.c) +- [Secure MPU tables and the SPM RAM cover](../src/arch/armv8m/mpu_armv8m.c) - [Guest verification](../src/guest_verify.c) - [HSM relay binding](../src/services/wolfhsm/wt_hsm.c) - [Native crypto dispatch](../src/services/native/crypto_native.c) diff --git a/docs/Testing.md b/docs/Testing.md index 84b4114e..14d63a0a 100644 --- a/docs/Testing.md +++ b/docs/Testing.md @@ -143,6 +143,23 @@ than their names suggest: parked on its stack top, so the exception frame lands on the seal words; that partition alone faults and restarts, and the guests keep running. +Three more cover the SPM's own fault handling: + +- `mspovfneg` pushes on the Secure main stack in the reset path until + `MSPLIM_S` raises STKOF; the platform halts on the production panic before + any partition or guest runs. +- `xnneg` makes the privileged SVC gate call a thunk copied into SPM `.bss` + while a partition thread domain is installed; the execute-never cover faults + the fetch and the SPM-origin fault halts the platform. +- `svcneg` has the ITS partition issue the scheduler's internal guest-return + SVC; the partition alone is panicked and restarted, and the lifecycle + completes. + +`busfaultneg` (the SERVICE_HSM partition reads an MPU-permitted window past +the end of physical SRAM) runs only on the STM32H563: M33MU turns an unmapped +data access into a MemManage fault and never vectors a data BusFault, so the +scenario has no emulator row until the pinned emulator models it. + VNET has convenience targets: ```sh From ed7983a8b71f5218df9516842b9c94ba329c5cb5 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 1 Oct 2026 10:56:33 -0700 Subject: [PATCH 8/9] Assert the SPM hardening negatives on M33MU and both BusFault paths on silicon --- docs/Testing.md | 18 ++-- .../apps/guest0_psa/CMakeLists.txt | 4 + .../zephyr-stm32h5/apps/guest0_psa/src/main.c | 19 ++++ .../zephyr-stm32h5/scripts/build_guest.sh | 2 + tests/target/lib/scenario.sh | 20 +++-- tests/target/run_h5_hardware.sh | 30 ++++++- tests/target/run_m33mu_scenario.sh | 87 +++++++++++++------ 7 files changed, 139 insertions(+), 41 deletions(-) diff --git a/docs/Testing.md b/docs/Testing.md index 14d63a0a..f2479a2d 100644 --- a/docs/Testing.md +++ b/docs/Testing.md @@ -146,19 +146,25 @@ than their names suggest: Three more cover the SPM's own fault handling: - `mspovfneg` pushes on the Secure main stack in the reset path until - `MSPLIM_S` raises STKOF; the platform halts on the production panic before - any partition or guest runs. + `MSPLIM_S` raises STKOF. M33MU escalates the entry-time STKOF to HardFault + and ends the run there without executing the handler, so the emulator + proves only the limit; the STM32H563 run reads the SPM fault latch and + checks that no guest ran. - `xnneg` makes the privileged SVC gate call a thunk copied into SPM `.bss` while a partition thread domain is installed; the execute-never cover faults - the fetch and the SPM-origin fault halts the platform. + the fetch. M33MU pends that synchronous fault instead of escalating it past + the active SVC, so the emulator proves only the denied fetch; the STM32H563 + run checks the SPM fault latch and the halt. - `svcneg` has the ITS partition issue the scheduler's internal guest-return SVC; the partition alone is panicked and restarted, and the lifecycle completes. `busfaultneg` (the SERVICE_HSM partition reads an MPU-permitted window past -the end of physical SRAM) runs only on the STM32H563: M33MU turns an unmapped -data access into a MemManage fault and never vectors a data BusFault, so the -scenario has no emulator row until the pinned emulator models it. +the end of physical SRAM) and `nsbusfaultneg` (guest0 turns off its own MPU and +reads an unmapped Non-secure peripheral hole; the monitor restarts it to its +limit while guest1 runs) run only on the STM32H563: M33MU turns an unmapped +data access into a MemManage fault and never vectors a data BusFault, so these +scenarios have no emulator row until the pinned emulator models it. VNET has convenience targets: diff --git a/tests/firmware/zephyr-stm32h5/apps/guest0_psa/CMakeLists.txt b/tests/firmware/zephyr-stm32h5/apps/guest0_psa/CMakeLists.txt index 72611894..06d8faba 100644 --- a/tests/firmware/zephyr-stm32h5/apps/guest0_psa/CMakeLists.txt +++ b/tests/firmware/zephyr-stm32h5/apps/guest0_psa/CMakeLists.txt @@ -68,6 +68,10 @@ if(WT_GUEST_FAULT_PROBE) target_compile_definitions(app PRIVATE WT_GUEST_FAULT_PROBE=1) endif() +if(WT_NS_BUSFAULT_PROBE) + target_compile_definitions(app PRIVATE WT_NS_BUSFAULT_PROBE=1) +endif() + if(WT_WRITE_ONCE_RESET_PROBE) target_compile_definitions(app PRIVATE WT_WRITE_ONCE_RESET_PROBE=1) endif() diff --git a/tests/firmware/zephyr-stm32h5/apps/guest0_psa/src/main.c b/tests/firmware/zephyr-stm32h5/apps/guest0_psa/src/main.c index d4e0b756..6c7198c8 100644 --- a/tests/firmware/zephyr-stm32h5/apps/guest0_psa/src/main.c +++ b/tests/firmware/zephyr-stm32h5/apps/guest0_psa/src/main.c @@ -1093,6 +1093,22 @@ static void wt_guest_fault_probe(void) } #endif +#if defined(WT_NS_BUSFAULT_PROBE) +/* Test-only: with its own MPU off, the guest reads an unmapped Non-secure hole; + * the precise BusFault goes to the Secure monitor (BFHFNMINS=0) for restart. */ +static void wt_guest_ns_busfault_probe(void) +{ + volatile uint32_t *mpu_ctrl_ns = (volatile uint32_t *)0xE000ED94u; + volatile const uint32_t *hole = (volatile const uint32_t *)0x4C000000u; + uint32_t sink; + + *mpu_ctrl_ns = 0u; + __asm volatile("dsb; isb"); + sink = *hole; + (void)sink; +} +#endif + #if defined(WT_FWU_PROBE) /* P6-S4: drive SERVICE_FWU from a Non-secure guest. The unprivileged FWU SP * stages the candidate through its privileged SVC flash gate and @@ -1546,6 +1562,9 @@ int main(void) #if defined(WT_GUEST_FAULT_PROBE) wt_guest_fault_probe(); #endif +#if defined(WT_NS_BUSFAULT_PROBE) + wt_guest_ns_busfault_probe(); +#endif rc = wt_zephyr_client_init("guest0_psa"); if (rc == 0) { diff --git a/tests/firmware/zephyr-stm32h5/scripts/build_guest.sh b/tests/firmware/zephyr-stm32h5/scripts/build_guest.sh index d5522af7..1bbde406 100755 --- a/tests/firmware/zephyr-stm32h5/scripts/build_guest.sh +++ b/tests/firmware/zephyr-stm32h5/scripts/build_guest.sh @@ -25,6 +25,7 @@ WT_EXPECTED_LIFECYCLE="${WT_EXPECTED_LIFECYCLE:-0x3000u}" WT_ATTESTATION_DEVELOPMENT_PROFILE="${WT_ATTESTATION_DEVELOPMENT_PROFILE:-0}" WT_M33MU_EXPECT_BKPT="${WT_M33MU_EXPECT_BKPT:-0}" WT_GUEST_FAULT_PROBE="${WT_GUEST_FAULT_PROBE:-0}" +WT_NS_BUSFAULT_PROBE="${WT_NS_BUSFAULT_PROBE:-0}" WT_ATTEST_NEG_PROBE="${WT_ATTEST_NEG_PROBE:-0}" WT_FWU_PROBE="${WT_FWU_PROBE:-0}" WT_WRITE_ONCE_RESET_PROBE="${WT_WRITE_ONCE_RESET_PROBE:-0}" @@ -61,6 +62,7 @@ set -- \ "-DWT_ATTESTATION_DEVELOPMENT_PROFILE=$WT_ATTESTATION_DEVELOPMENT_PROFILE" \ "-DWT_M33MU_EXPECT_BKPT=$WT_M33MU_EXPECT_BKPT" \ "-DWT_GUEST_FAULT_PROBE=$WT_GUEST_FAULT_PROBE" \ + "-DWT_NS_BUSFAULT_PROBE=$WT_NS_BUSFAULT_PROBE" \ "-DWT_ATTEST_NEG_PROBE=$WT_ATTEST_NEG_PROBE" \ "-DWT_FWU_PROBE=$WT_FWU_PROBE" \ "-DWT_WRITE_ONCE_RESET_PROBE=$WT_WRITE_ONCE_RESET_PROBE" \ diff --git a/tests/target/lib/scenario.sh b/tests/target/lib/scenario.sh index 5276dd3a..2befedcb 100644 --- a/tests/target/lib/scenario.sh +++ b/tests/target/lib/scenario.sh @@ -61,7 +61,7 @@ scenario_secure_flags() { scenario_end() { case "$1" in rollbackneg|spbudgetneg) echo "bkpt:0x7d" ;; - manifestneg|manifestneg2|sealbootneg|mspovfneg|xnneg) echo "bkpt:0x7e" ;; + manifestneg|manifestneg2|sealbootneg) echo "bkpt:0x7e" ;; sealhaltneg) echo "bkpt:0x6e" ;; remeasureneg) echo "bkpt:0x6c" ;; *) echo "idle" ;; @@ -106,19 +106,23 @@ scenario_assert_verdict() { "$GUEST_DONE_RE" ;; xnneg) + # M33MU pends a synchronous fault that cannot preempt the active + # SVC instead of escalating it, so the production halt is + # asserted on silicon; the emulator proves the fetch was denied. expect_re "privileged execution from SPM RAM faulted (IACCVIOL)" \ '\[MEMFAULT\] pc=0x30[0-9a-f]{6} addr=0x30[0-9a-f]{6}' refute_re "the thunk never returned into the gate" '\[USGFLT\]' - expect "SPM-origin fault halted the platform fail-closed" \ - "[BKPT] imm=0x7e" - refute_re "no guest scheduled after the halt" \ - "$GUEST_STARTED_RE" + refute_re "no clean lifecycle after the SPM fault" \ + '\[BKPT\] imm=0x7f' ;; mspovfneg) + # M33MU escalates the entry-time STKOF to HardFault and ends the + # run there without executing the handler; the production halt + # is asserted on silicon through the SPM fault latch. expect_re "main-stack overflow raised STKOF against MSPLIM_S" \ - '\[USGFLT\].*CFSR=0x00[1-9a-f][0-9a-f]0000' - expect "SPM overflow halted the platform fail-closed" \ - "[BKPT] imm=0x7e" + '\[(USGFLT|HARDFLT)\].*CFSR=0x00[1-9a-f][0-9a-f]0000' + expect "the emulator ended the run at the SPM fault" \ + "Execution stopped" refute_re "no guest scheduled after the refused boot" \ "$GUEST_STARTED_RE" ;; diff --git a/tests/target/run_h5_hardware.sh b/tests/target/run_h5_hardware.sh index 5dab2ddf..a8918215 100755 --- a/tests/target/run_h5_hardware.sh +++ b/tests/target/run_h5_hardware.sh @@ -42,7 +42,7 @@ set -o pipefail mode="${1:-all}" scenario="${2:-positive}" case "$mode" in build|flash|all) ;; *) echo "usage: $0 build|flash|all [scenario]" >&2; exit 2 ;; esac -case "$scenario" in positive|restart|crossdomain|keystoreneg|panicneg|confboot|devstorage|devcrypto|devattest|devattestqcbor|vaultrecover|vaultrecoversec|authneg|writeonce|hsmattackneg|bootupdate|vnet|vnetneg|gtzcneg|fpneg|sealneg|sealpivotneg|periphneg|mspovfneg|busfaultneg|xnneg|svcneg) ;; *) echo "usage: $0 $mode positive|restart|crossdomain|keystoreneg|panicneg|confboot|devstorage|devcrypto|devattest|devattestqcbor|vaultrecover|vaultrecoversec|authneg|writeonce|hsmattackneg|bootupdate|vnet|vnetneg|gtzcneg|fpneg|sealneg|sealpivotneg|periphneg|mspovfneg|busfaultneg|xnneg|svcneg" >&2; exit 2 ;; esac +case "$scenario" in positive|restart|crossdomain|keystoreneg|panicneg|confboot|devstorage|devcrypto|devattest|devattestqcbor|vaultrecover|vaultrecoversec|authneg|writeonce|hsmattackneg|bootupdate|vnet|vnetneg|gtzcneg|fpneg|sealneg|sealpivotneg|periphneg|mspovfneg|busfaultneg|nsbusfaultneg|xnneg|svcneg) ;; *) echo "usage: $0 $mode positive|restart|crossdomain|keystoreneg|panicneg|confboot|devstorage|devcrypto|devattest|devattestqcbor|vaultrecover|vaultrecoversec|authneg|writeonce|hsmattackneg|bootupdate|vnet|vnetneg|gtzcneg|fpneg|sealneg|sealpivotneg|periphneg|mspovfneg|busfaultneg|nsbusfaultneg|xnneg|svcneg" >&2; exit 2 ;; esac repo="$(cd "$(dirname "$0")/../.." && pwd)" cd "$repo" @@ -60,7 +60,7 @@ SERIAL="${H5_SERIAL:-/dev/ttyACM0}" # confboot reboots the whole chain once per panic test (real SYSRESETREQ, each # re-running wolfBoot), so it needs a long ceiling; the capture stops early on # the suite report. -case "$scenario" in restart) cap_default=32 ;; confboot) cap_default=900 ;; devstorage|devcrypto|devattest|devattestqcbor|vaultrecover|vaultrecoversec) cap_default=600 ;; bootupdate) cap_default=45 ;; authneg) cap_default=30 ;; writeonce) cap_default=40 ;; *) cap_default=25 ;; esac +case "$scenario" in restart|nsbusfaultneg) cap_default=32 ;; confboot) cap_default=900 ;; devstorage|devcrypto|devattest|devattestqcbor|vaultrecover|vaultrecoversec) cap_default=600 ;; bootupdate) cap_default=45 ;; authneg) cap_default=30 ;; writeonce) cap_default=40 ;; *) cap_default=25 ;; esac CAP_S="${H5_CAPTURE_SECONDS:-$cap_default}" LOGFILE="${WT_SCENARIO_LOG:-ci-h5-hardware-$scenario.log}" case "$LOGFILE" in /*) ;; *) LOGFILE="$repo/$LOGFILE" ;; esac @@ -162,6 +162,7 @@ if [ "$mode" != "flash" ]; then [ "$scenario" = "keystoreneg" ] && secure_flags="WT_KEYSTORE_NEG_PROBE=1" [ "$scenario" = "panicneg" ] && secure_flags="WT_PANIC_NEG_PROBE=1" [ "$scenario" = "restart" ] && guest_flags="WT_GUEST_FAULT_PROBE=1" + [ "$scenario" = "nsbusfaultneg" ] && guest_flags="WT_NS_BUSFAULT_PROBE=1" [ "$scenario" = "writeonce" ] && guest_flags="WT_WRITE_ONCE_RESET_PROBE=1" [ "$scenario" = "hsmattackneg" ] && guest_flags="WT_HSM_ATTACK_PROBE=1" [ "$scenario" = "gtzcneg" ] && guest_flags="WT_MPU_BYPASS_PROBE=1" @@ -608,6 +609,31 @@ if [ "$mode" != "build" ]; then fi expect "guest1 alive after guest0 FAULTED" "freertos_guest1: heartbeat" ;; + nsbusfaultneg) + # guest0 reads an unmapped Non-secure peripheral hole each boot: a precise + # BusFault the Secure monitor attributes to guest0 (BFAR), restarting it + # RESTART_LIMIT times then quarantining it while guest1 keeps running. + refute_re "no HardFault escalation" '^(\[HARDFLT\]|HardFault|SecureFault)' + fault_addr=$(read_secure_u32 g_last_fault_address) + if [ -n "$fault_addr" ] && [ $((0x$fault_addr)) -eq $((0x4C000000)) ]; then + check_pass "BusFault BFAR names the Non-secure hole (0x$fault_addr)" + else + check_fail "Non-secure BusFault" "fault addr 0x${fault_addr:-none}, want 0x4C000000" + fi + restarts=$(read_secure_u32 g_wt_restart_events) + quarantines=$(read_secure_u32 g_wt_quarantine_events) + if [ -n "$restarts" ] && [ $((0x$restarts)) -eq "$RESTART_LIMIT" ]; then + check_pass "monitor restarted guest0 exactly $RESTART_LIMIT times" + else + check_fail "guest restart count" "restart events 0x${restarts:-none}, expected $RESTART_LIMIT" + fi + if [ -n "$quarantines" ] && [ $((0x$quarantines)) -eq 1 ]; then + check_pass "guest0 quarantined after the limit (events=1)" + else + check_fail "quarantine" "quarantine events 0x${quarantines:-none}, expected 1" + fi + expect "guest1 alive through guest0's BusFaults" "freertos_guest1: heartbeat" + ;; crossdomain) # The unprivileged crypto SP reads SPM-private RAM (WT_RAM_S_BASE) on # entry; its MPU domain denies it. With graceful quarantine the fault diff --git a/tests/target/run_m33mu_scenario.sh b/tests/target/run_m33mu_scenario.sh index d82a6cff..1e2e1556 100755 --- a/tests/target/run_m33mu_scenario.sh +++ b/tests/target/run_m33mu_scenario.sh @@ -296,16 +296,23 @@ elif [ "$scenario" = "sealbootneg" ]; then # The reset path refuses the damaged main-stack seal before any partition. expect_bkpt=0x7e timeout_s=40 -elif [ "$scenario" = "mspovfneg" ] || [ "$scenario" = "xnneg" ]; then - # The SPM faults itself on purpose (main-stack overflow against MSPLIM_S, - # or privileged execution from its own RAM) and halts on the production - # panic. Do not quit on the fault. +elif [ "$scenario" = "mspovfneg" ]; then + # The reset path overflows the main stack on purpose. M33MU escalates the + # entry-time STKOF to HardFault and ends the run there, so the production + # halt (BKPT 0x7E) is never reached on the emulator; see the H5 run. + quit_flag="" + timeout_s=40 +elif [ "$scenario" = "xnneg" ]; then + # The privileged gate executes from SPM RAM on purpose. M33MU pends the + # synchronous fault instead of escalating it past the active SVC, so the + # production halt (BKPT 0x7E) is proven on the H5; the emulator proves the + # denied fetch. Do not quit on the fault. quit_flag="" - expect_bkpt=0x7e timeout_s=40 elif [ "$scenario" = "spfaultneg" ] || [ "$scenario" = "panicneg" ] || [ "$scenario" = "vnetneg" ] || [ "$scenario" = "sealneg" ] || - [ "$scenario" = "sealpivotneg" ] || [ "$scenario" = "svcneg" ]; then + [ "$scenario" = "sealpivotneg" ] || [ "$scenario" = "svcneg" ] || + [ "$scenario" = "busfaultneg" ]; then # The SP faults on purpose; wolfTrust catches the fault and restarts # the partition in place, so halting on the fault would defeat the # recovery. The rest of the lifecycle then completes normally through the @@ -370,11 +377,14 @@ check_pass() { printf ' [check] PASS %s\n' "$1"; } check_fail() { printf ' [check] FAIL %s (%s)\n' "$1" "$2"; exit 1; } expect() { if grep -Fq "$2" "$log"; then check_pass "$1"; \ else check_fail "$1" "missing: $2"; fi; } -# Shared-UART tolerant match: guest1's console can interject mid-line in a -# secure print (e.g. "TOTAL SKIPPED : 4"), so strip -# guest1 text and rejoin split lines before requiring the exact bytes. -expect_flat() { if sed 's/freertos_guest1:.*$//' "$log" | tr -d '\r\n' | \ - grep -Fq "$2"; then check_pass "$1"; \ +# Shared-UART tolerant match: guest1's console, or the emulator's own +# "[UART] ... attached" note when guest1 first opens it, can interject +# mid-line in a secure or guest0 print (e.g. "TOTAL SKIPPED : 4"), so strip both and rejoin split lines before requiring +# the exact bytes. +expect_flat() { if sed -e 's/freertos_guest1:.*$//' \ + -e 's/\[UART\] [0-9a-f]* attached to [^ ]*//' "$log" | \ + tr -d '\r\n' | grep -Fq "$2"; then check_pass "$1"; \ else check_fail "$1" "missing: $2"; fi; } refute_re() { if grep -Eq "$2" "$log"; then check_fail "$1" "unexpected: $2"; \ else check_pass "$1"; fi; } @@ -437,9 +447,9 @@ case "$scenario" in "wolfTrust key-ops sign/verify verified" expect "key negatives verified" \ "wolfTrust key negatives verified" - expect "forged-handle call rejected" \ + expect_flat "forged-handle call rejected" \ "wolfTrust FF-M forged-handle call rejected" - expect "oversized-vector call rejected" \ + expect_flat "oversized-vector call rejected" \ "wolfTrust FF-M oversized-vector call rejected" expect "psa_hash_compute(SHA-256) KAT verified" \ "psa_hash_compute(SHA-256) KAT verified" @@ -1011,6 +1021,32 @@ case "$scenario" in expect "run reached the clean scenario end" "[EXPECT BKPT] Success" echo "PASS: target/sealpivotneg" ;; + busfaultneg) + # The SERVICE_HSM relay SP reads an MPU-permitted window past the end of + # physical SRAM on its first entry: a precise BusFault with BFAR that the + # SPM attributes to the partition, which restarts in place and then + # serves both OS clients. Needs an emulator that vectors a data BusFault + # (the matrix row lands with that pin bump). + if grep -Eq '\[BUSFLT\] pc=0x[0-9a-f]+ addr=0x300a0000' "$log"; then + check_pass "relay SP took a precise BusFault at the bus-error window" + else + check_fail "BusFault" "expected [BUSFLT] at 0x300a0000, none seen" + fi + refute_re "fault was contained, not escalated" \ + '(\[HARDFLT\]|HardFault|SecureFault)' + refute_re "platform did not halt on the partition's fault" \ + '\[BKPT\] imm=0x(6e|7e|7d)' + expect "restarted relay serves mediated key-ops" \ + "wolfTrust key-ops sign/verify verified" + expect "restarted relay serves the mediated SHA KAT" \ + "psa_hash_compute(SHA-256) KAT verified" + expect "unrelated guest booted and ran through the SP fault" \ + "freertos_guest1: alive" + expect "restarted relay serves the other-OS client too" \ + "freertos_guest1: ffm sha256 ok" + expect "full lifecycle completed after recovery" "[EXPECT BKPT] Success" + echo "PASS: target/busfaultneg" + ;; svcneg) # The ITS SP issues the scheduler's internal guest-return SVC (0x7F) on # its first entry. A PSP-origin caller is a PROGRAMMER ERROR: the SPM @@ -1049,25 +1085,26 @@ case "$scenario" in check_fail "XN" "expected an instruction-fetch MemManage in SPM RAM, none seen" fi refute_re "the thunk never returned into the gate" '\[USGFLT\]' - expect "SPM-origin fault halted the platform fail-closed" "[BKPT] imm=0x7e" - expect "the emulator stopped on that halt" "[EXPECT BKPT] Success" - refute_re "no guest ran after the halt" \ - '(guest0_psa alive|freertos_guest1:|vnet-guest|\[BKPT\] imm=0x7f)' - echo "PASS: target/xnneg" + refute_re "no clean lifecycle after the SPM fault" '\[BKPT\] imm=0x7f' + printf ' [check] INFO the production halt is not executed by the emulator (synchronous fault pended past the SVC); see the H5 xnneg run\n' + echo "PASS: target/xnneg (fetch denial only)" ;; mspovfneg) # The SPM pushes on its own stack until MSPLIM_S raises STKOF (CFSR bit - # 20, no partition frame to blame); the stackless halt must land on the - # production panic before any partition or guest runs. - if grep -Eq '\[USGFLT\].*CFSR=0x00[1-9a-f][0-9a-f]0000' "$log"; then + # 20, no partition frame to blame). M33MU escalates the entry-time STKOF + # to HardFault and ends the run without executing the handler, so the + # emulator proves the limit; the H5 run proves the latch and the halt. + if grep -Eq '\[(USGFLT|HARDFLT)\].*CFSR=0x00[1-9a-f][0-9a-f]0000' "$log"; then check_pass "main-stack overflow raised STKOF against MSPLIM_S" else - check_fail "STKOF" "expected a STKOF UsageFault (CFSR bit 20), none seen" + check_fail "STKOF" "expected a STKOF fault (CFSR bit 20), none seen" fi - expect "SPM overflow halted the platform fail-closed" "[BKPT] imm=0x7e" - expect "the emulator stopped on that halt" "[EXPECT BKPT] Success" - refute_re "no partition or guest ran after the halt" \ + expect "the emulator ended the run at the SPM fault, not on the wall clock" \ + "Execution stopped" + refute_re "run did not reach the wall-clock budget" 'wall-clock' + refute_re "no partition or guest ran after the fault" \ '(guest0_psa alive|freertos_guest1:|vnet-guest|\[BKPT\] imm=0x7f)' + printf ' [check] INFO the production halt is not executed by the emulator; see the H5 mspovfneg run\n' echo "PASS: target/mspovfneg" ;; sealhaltneg) From b15a77b1ea3df45b4aba86b8bc7127dfc74f44fa Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 2 Oct 2026 08:56:15 -0700 Subject: [PATCH 9/9] Keep the first SPM fault record and clear guest escalation status --- src/arch/armv8m/guest_context_armv8m.c | 6 +++++- src/arch/armv8m/sp_fault_armv8m.c | 25 +++++++++++++++++-------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/arch/armv8m/guest_context_armv8m.c b/src/arch/armv8m/guest_context_armv8m.c index 46034f72..2d3fa26d 100644 --- a/src/arch/armv8m/guest_context_armv8m.c +++ b/src/arch/armv8m/guest_context_armv8m.c @@ -392,8 +392,12 @@ static void wt_secure_hardfault_dispatch(void) __attribute__((noreturn, used)); static void wt_secure_hardfault_dispatch(void) { + uint32_t cfsr = WT_SCB_CFSR_S; + uint32_t hfsr = WT_SCB_HFSR_S; + g_last_fault_address = 0u; - WT_SCB_HFSR_S = WT_SCB_HFSR_FORCED; + WT_SCB_CFSR_S = cfsr & WT_SCB_CFSR_BFSR_MASK; + WT_SCB_HFSR_S = hfsr; wt_monitor_on_guest_fault(&g_wt_guest_no_frame, WT_FAULT_SECURE_ESCALATION); wt_platform_panic(); __builtin_unreachable(); diff --git a/src/arch/armv8m/sp_fault_armv8m.c b/src/arch/armv8m/sp_fault_armv8m.c index 731c7eb3..39ac2d9b 100644 --- a/src/arch/armv8m/sp_fault_armv8m.c +++ b/src/arch/armv8m/sp_fault_armv8m.c @@ -62,11 +62,16 @@ volatile uint32_t g_wt_spm_fault_pc __attribute__((used)); volatile uint32_t g_wt_spm_fault_exc_return __attribute__((used)); /* Stackless: a main-stack overflow or HardFault may arrive with MSP at or - * under its limit, so the latch is written in asm and the halt tail-called. */ + * under its limit, so the latch is written in asm and the halt tail-called. + * First fault wins: the panic BKPT escalating without a debugger keeps it. */ __attribute__((naked, noreturn, used)) void wt_armv8m_spm_fault_halt(void) { __asm volatile( + "ldr r2, =g_wt_spm_fault_exc_return \n" + "ldr r1, [r2] \n" + "cmp r1, #0 \n" + "bne 2f \n" "ldr r0, =0xE000ED28 \n" "ldr r1, [r0, #0] \n" "ldr r2, =g_wt_spm_fault_cfsr \n" @@ -87,7 +92,7 @@ void wt_armv8m_spm_fault_halt(void) "ldr r1, [r0, #0] \n" "ldr r2, =0x00101818 \n" "tst r1, r2 \n" - "movs r1, #0 \n" + "mov r1, #0 \n" "bne 1f \n" "tst lr, #0x40 \n" "beq 1f \n" @@ -99,6 +104,7 @@ void wt_armv8m_spm_fault_halt(void) "1: \n" "ldr r2, =g_wt_spm_fault_pc \n" "str r1, [r2] \n" + "2: \n" "b wt_platform_panic \n" ); } @@ -119,12 +125,15 @@ __attribute__((naked)) void HardFault_Handler(void) static void wt_spm_fault_latch(uint32_t cfsr, const uint32_t *frame, uint32_t exc_return) { - g_wt_spm_fault_cfsr = cfsr; - g_wt_spm_fault_hfsr = WT_SCB_HFSR_S; - g_wt_spm_fault_mmfar = WT_SCB_MMFAR_S; - g_wt_spm_fault_bfar = WT_SCB_BFAR_S; - g_wt_spm_fault_pc = ((cfsr & WT_SCB_CFSR_NO_FRAME) == 0u) ? frame[6] : 0u; - g_wt_spm_fault_exc_return = exc_return; + if (g_wt_spm_fault_exc_return == 0u) { + g_wt_spm_fault_cfsr = cfsr; + g_wt_spm_fault_hfsr = WT_SCB_HFSR_S; + g_wt_spm_fault_mmfar = WT_SCB_MMFAR_S; + g_wt_spm_fault_bfar = WT_SCB_BFAR_S; + g_wt_spm_fault_pc = ((cfsr & WT_SCB_CFSR_NO_FRAME) == 0u) ? + frame[6] : 0u; + g_wt_spm_fault_exc_return = exc_return; + } wt_platform_panic(); }