From 69fbb472114c2a4cd85ea0b93ef04da2bfc112fc Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 23 Oct 2026 11:09:56 -0700 Subject: [PATCH 1/2] F-15754 - Allow guest restart backoff without a runnable peer --- docs/Architecture.md | 4 + docs/Security-Model.md | 4 + docs/Testing.md | 7 + include/wolftrust/arch.h | 4 + src/arch/armv8m/guest_context_armv8m.c | 21 +++ src/monitor.c | 23 ++- tests/host/Makefile | 2 +- tests/host/monitor/Makefile | 54 ++++++ tests/host/monitor/main.c | 252 +++++++++++++++++++++++++ tests/target/lib/scenario_matrix.py | 2 +- tests/target/run_m33mu_scenario.sh | 19 +- 11 files changed, 386 insertions(+), 6 deletions(-) create mode 100644 tests/host/monitor/Makefile create mode 100644 tests/host/monitor/main.c diff --git a/docs/Architecture.md b/docs/Architecture.md index 697ace942..8e77e18fc 100644 --- a/docs/Architecture.md +++ b/docs/Architecture.md @@ -153,6 +153,10 @@ clears restart-marked RAM and restores a clean context after a bounded delay; if the budget is exhausted, the guest remains faulted. Runtime remeasurement can also quarantine a guest without entering it. +When no guest is runnable but recovery is pending, the monitor idles in +Secure Thread mode so Secure SysTick can finish the backoff. It halts only +when every guest is faulted or stopped. + The PSA lifecycle value comes from the authenticated boot handoff. Scheduled Secure Partitions can read it through `psa_rot_lifecycle_state()`; there is no Non-secure client adapter or CMSE veneer for that function. The value also controls diff --git a/docs/Security-Model.md b/docs/Security-Model.md index 661034586..8f92fd5f7 100644 --- a/docs/Security-Model.md +++ b/docs/Security-Model.md @@ -97,6 +97,10 @@ for restart and reconstructs the initial context. The image is verified again before the guest runs; failed verification or an exhausted restart budget leaves the guest quarantined. +Pending recovery keeps the Secure timer running even when no peer is runnable. +The monitor leaves the fault handler and waits in Secure Thread mode; a fully +faulted or stopped guest set remains terminal. + ## Secure Partition isolation Every shipped service loop runs as a scheduled unprivileged Secure coroutine. diff --git a/docs/Testing.md b/docs/Testing.md index c35de758a..fa15ff783 100644 --- a/docs/Testing.md +++ b/docs/Testing.md @@ -32,6 +32,9 @@ vectors, Secure Partition layout and recovery, crypto-engine relay and key isolation, vault and storage services, attestation and COSE integration, firmware update, runtime remeasurement, linked Secure layout, VNET, public PSA headers, boot-handoff record consumption, and negative paths. +The monitor suite checks bounded guest recovery, idle backoff, and terminal +lifecycle decisions against the production scheduler with simulated architecture +operations. The attestation IAK suite runs wolfHSM NVM with both the default 8-byte and STM32H5 16-byte flash programming units. @@ -122,6 +125,10 @@ authenticated-boot failure, rollback, runtime remeasurement, Secure Partition recovery, key and vault isolation, storage recovery, attestation negatives, firmware update, manifest rejection, GTZC behavior, and VNET paths. +`restartidle` runs the STM32H563 recovery check with `WT_MAX_GUESTS=1`. It +requires every allowed relaunch and the final terminal verdict. The full tier +runs it with both crypto engines. + Five scenarios cover processor-state isolation, and the emulator proves less than their names suggest: diff --git a/include/wolftrust/arch.h b/include/wolftrust/arch.h index 84f1a46a4..c1893791d 100644 --- a/include/wolftrust/arch.h +++ b/include/wolftrust/arch.h @@ -64,6 +64,10 @@ bool wt_arch_trap_from_guest_thread(void); bool wt_arch_trap_from_secure_thread(void); void wt_arch_return_to_secure_thread( void (*entry)(void) __attribute__((noreturn))); +/* Discard the departing handler stack before entering the idle scheduler. */ +void wt_arch_return_to_idle_thread( + void (*entry)(void) __attribute__((noreturn))) __attribute__((noreturn)); +void wt_arch_wait_for_interrupt(void); uintptr_t wt_arch_read_fault_address(void); diff --git a/src/arch/armv8m/guest_context_armv8m.c b/src/arch/armv8m/guest_context_armv8m.c index 2d3fa26dd..e68d90d5a 100644 --- a/src/arch/armv8m/guest_context_armv8m.c +++ b/src/arch/armv8m/guest_context_armv8m.c @@ -272,6 +272,27 @@ void wt_arch_return_to_secure_thread( "b wt_platform_panic \n"); } +/* Idle has no suspended Secure caller; reuse the sealed main stack. */ +__attribute__((naked, noreturn)) +void wt_arch_return_to_idle_thread( + void (*entry)(void) __attribute__((noreturn, unused))) +{ + __asm volatile( + "ldr r1, =_estack \n" + "sub r1, r1, #8 \n" + "mov sp, r1 \n" + "b wt_arch_return_to_secure_thread \n"); +} + +void wt_arch_wait_for_interrupt(void) +{ + /* No Non-secure context owns the hardware timer while idle. */ + WT_SYST_NS_CSR = 0U; + WT_SCB_ICSR_NS = WT_SCB_ICSR_PENDSTCLR; + g_active_guest = UINT32_MAX; + __asm volatile("dsb 0xF\n wfi\n isb 0xF" ::: "memory"); +} + static void wt_exception_return_ns_msp(void) __attribute__((naked, noreturn)); static void wt_exception_return_ns_msp(void) diff --git a/src/monitor.c b/src/monitor.c index 01f88dd99..520a1581e 100644 --- a/src/monitor.c +++ b/src/monitor.c @@ -441,6 +441,19 @@ static void wt_restart_guest(wt_guest_id_t guest_id, wt_fault_reason_t reason) } +static bool wt_all_guests_terminal(void) +{ + size_t i; + + for (i = 0U; i < g_scheduler.guest_count; ++i) { + if (g_scheduler.runtime[i].state != WT_GUEST_FAULTED && + g_scheduler.runtime[i].state != WT_GUEST_STOPPED) { + return false; + } + } + return true; +} + static void wt_schedule_next_guest(void) { wt_guest_id_t next_guest; @@ -456,7 +469,15 @@ static void wt_schedule_next_guest(void) &rep); if (next_guest >= g_scheduler.guest_count) { - wt_platform_all_guests_faulted(); + if (wt_all_guests_terminal()) { + wt_platform_all_guests_faulted(); + } + /* Backoff must run after the fault handler has unwound. */ + if (wt_arch_in_handler_mode()) { + wt_arch_return_to_idle_thread(wt_schedule_next_guest); + } + wt_arch_wait_for_interrupt(); + continue; } wt_arch_quarantine_pending_irqs(&g_scheduler.configs[next_guest].irq_mask); diff --git a/tests/host/Makefile b/tests/host/Makefile index 4e82e96a8..cf86f5ba6 100644 --- a/tests/host/Makefile +++ b/tests/host/Makefile @@ -31,7 +31,7 @@ else SUBMAKE_FLAGS := --no-print-directory -s endif -UNIT_SUITES := domain manifest lifecycle guest_verify rollback sp_recovery \ +UNIT_SUITES := domain manifest lifecycle monitor guest_verify rollback sp_recovery \ fabric_windows periph irq_claim rt700_trng rt700_xspi \ ipc ffm spm \ vnet wolfhsm_loopback wolfhsm_relay native_wire native_wire_client \ diff --git a/tests/host/monitor/Makefile b/tests/host/monitor/Makefile new file mode 100644 index 000000000..13a972604 --- /dev/null +++ b/tests/host/monitor/Makefile @@ -0,0 +1,54 @@ +# Makefile +# +# Copyright (C) 2026 wolfSSL Inc. +# +# This file is part of wolfTrust. +# +# wolfTrust is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# wolfTrust is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, see . + +ROOT := ../../.. +CC ?= cc +BUILD_DIR ?= build +CFLAGS := -std=c99 -O0 -g -Wall -Wextra -Werror -pedantic \ + -Wno-unused-function -ffunction-sections -fdata-sections \ + -I$(ROOT)/include -I$(BUILD_DIR) $(EXTRA_CFLAGS) +ifeq ($(shell uname -s),Darwin) +GC_FLAGS := -Wl,-dead_strip +else +GC_FLAGS := -Wl,--gc-sections +endif +TEST := $(BUILD_DIR)/test_monitor +GENERATOR := $(ROOT)/tools/manifest/generate.py +FIXTURE := $(ROOT)/port/stm32h563/manifest.json +GENERATED := $(BUILD_DIR)/wolftrust_manifest_generated.h + +.PHONY: all run sanitize valgrind clean +all: $(TEST) +$(GENERATED): $(GENERATOR) $(FIXTURE) + mkdir -p $(BUILD_DIR) + python3 $(GENERATOR) $(FIXTURE) $(BUILD_DIR) \ + --supported-features 0x1 --address-bits 32 +$(TEST): main.c $(ROOT)/src/monitor.c $(ROOT)/src/restart_policy.c $(GENERATED) + $(CC) $(CFLAGS) $(GC_FLAGS) $(EXTRA_LDFLAGS) -o $@ \ + main.c $(ROOT)/src/restart_policy.c +run: $(TEST) + $(TEST) +sanitize: + $(MAKE) run CC=clang BUILD_DIR=$(BUILD_DIR)/sanitize \ + EXTRA_CFLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" \ + EXTRA_LDFLAGS="-fsanitize=address,undefined" +valgrind: $(TEST) + valgrind --error-exitcode=1 --leak-check=full $(TEST) +clean: + rm -rf $(BUILD_DIR) diff --git a/tests/host/monitor/main.c b/tests/host/monitor/main.c new file mode 100644 index 000000000..9d8973448 --- /dev/null +++ b/tests/host/monitor/main.c @@ -0,0 +1,252 @@ +/* main.c + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfTrust. + * + * wolfTrust is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfTrust is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +#include +#include +#include + +#include "wolftrust/arch.h" + +/* The architecture boundary is simulated; the scheduler itself is production. */ +#include "../../../src/monitor.c" + +enum { DISPATCHED = 1, HALTED, PANICKED, TIMED_OUT }; +static jmp_buf g_exit; +static int g_event; +static wt_guest_config_t g_configs[2]; +static wt_guest_runtime_t g_runtime[2]; +static int g_contexts[2]; +static int g_failures; +static unsigned int g_idle_ticks; +static unsigned int g_idle_entries; +static unsigned int g_resets; +static bool g_handler; +static int g_verify_result; +static wt_guest_measurement_t g_record; + +static void check(int ok, const char* name) +{ + (void)printf("%s: WT-SYS-0008 %s\n", ok ? "PASS" : "FAIL", name); + if (!ok) { + g_failures++; + } +} + +static void setup(size_t count, uint32_t delay) +{ + size_t i; + + (void)memset(g_configs, 0, sizeof(g_configs)); + (void)memset(g_runtime, 0, sizeof(g_runtime)); + (void)memset(&g_scheduler, 0, sizeof(g_scheduler)); + g_scheduler.configs = g_configs; + g_scheduler.runtime = g_runtime; + g_scheduler.guest_count = count; + for (i = 0U; i < count; ++i) { + g_configs[i].guest_id = (wt_guest_id_t)i; + g_configs[i].timeslice_ms = 1U; + g_configs[i].restart_policy.restart_limit = 3U; + g_configs[i].restart_policy.initial_delay_ticks = delay; + g_runtime[i].context = (wt_guest_context_t*)&g_contexts[i]; + g_runtime[i].state = WT_GUEST_RUNNING; + } + g_idle_ticks = 0U; + g_idle_entries = 0U; + g_resets = 0U; + g_handler = true; + g_verify_result = WT_GUEST_VERIFY_OK; + g_wt_first_dispatch_pending = 0U; +} + +static void finish(int event) __attribute__((noreturn)); +static void finish(int event) +{ + g_event = event; + longjmp(g_exit, 1); +} + +static int fault_current(void) +{ + if (setjmp(g_exit) == 0) { + wt_monitor_on_guest_fault(NULL, WT_FAULT_MEMORY_VIOLATION); + return PANICKED; + } + return g_event; +} + +static int schedule(void) +{ + if (setjmp(g_exit) == 0) { + wt_schedule_next_guest(); + } + return g_event; +} + +void wt_arch_return_to_idle_thread( + void (*entry)(void) __attribute__((noreturn))) +{ + if (!g_handler) { + finish(PANICKED); + } + g_idle_entries++; + g_handler = false; + entry(); +} + +void wt_arch_wait_for_interrupt(void) +{ + if (g_handler) { + finish(PANICKED); + } + if (++g_idle_ticks > 8U) { + finish(TIMED_OUT); + } + wt_monitor_on_secure_timer(NULL); +} + +bool wt_arch_in_handler_mode(void) { return g_handler; } +bool wt_arch_trap_from_guest_thread(void) { return false; } +void wt_arch_mask_all_guest_irqs(void) {} +void wt_arch_quarantine_pending_irqs(const wt_irq_mask_t* mask) { (void)mask; } +void wt_arch_apply_irq_mask(const wt_irq_mask_t* mask) { (void)mask; } +void wt_arch_program_guest_domain(const wt_memory_region_t* r, size_t n) +{ (void)r; (void)n; } +void wt_platform_program_memory_windows(const wt_memory_window_t* w, size_t n) +{ (void)w; (void)n; } +void wt_arch_start_secure_timer(uint32_t ms) { (void)ms; } +void wt_arch_guest_context_prepare(wt_guest_id_t id, const wt_guest_context_t* c) +{ (void)id; (void)c; } +void wt_arch_guest_context_capture(wt_guest_context_t* c, + const wt_trap_frame_t* f) +{ (void)c; (void)f; } +void wt_arch_guest_context_restore(wt_guest_context_t* c) +{ (void)c; finish(DISPATCHED); } +void wt_arch_zero_guest_memory(uintptr_t base, size_t size) +{ (void)base; (void)size; } +uintptr_t wt_arch_read_fault_address(void) { return 0U; } +uintptr_t wt_arch_trap_pc(const wt_trap_frame_t* f) { (void)f; return 0U; } +void wt_platform_log_fault(wt_guest_id_t id, wt_fault_reason_t r, + uintptr_t a, uintptr_t pc) +{ (void)id; (void)r; (void)a; (void)pc; } +void wt_platform_all_guests_faulted(void) { finish(HALTED); } +void wt_platform_panic(void) { finish(PANICKED); } +wt_ffm_runtime_t* wt_ffm_boot_runtime_mut(void) { return NULL; } +int wt_ffm_fail_client_connections(wt_ffm_runtime_t* r, psa_client_id_t id) +{ (void)r; (void)id; return 0; } + +void wt_partition_reset_runtime(const wt_guest_config_t* c, + wt_guest_runtime_t* r) +{ + (void)c; + r->state = WT_GUEST_READY; + r->remaining_delay_ticks = 0U; + g_resets++; +} + +const wt_guest_measurement_t* wt_platform_guest_measurements(size_t* count) +{ *count = 1U; return &g_record; } +int wt_guest_verify_image(const void* p, size_t n, + const wt_guest_measurement_t* r, uint32_t version) +{ (void)p; (void)n; (void)r; (void)version; return g_verify_result; } +size_t wt_guest_measurement_count(void) { return 0U; } +const wt_guest_measurement_t* wt_guest_measurement_get(size_t i, + const char** name) +{ (void)i; (void)name; return NULL; } +int wt_guest_measurement_record(const wt_guest_measurement_t* r, + const char* name) +{ (void)r; (void)name; return 0; } + +int main(void) +{ + int event; + + setup(1U, 2U); + event = fault_current(); + check(event == DISPATCHED && g_idle_entries == 1U && + g_idle_ticks == 2U && g_resets == 1U && + g_runtime[0].restart_count == 1U, + "guest recovery completes its bounded backoff"); + + setup(1U, 0U); + event = fault_current(); + check(event == DISPATCHED && g_idle_entries == 1U && g_idle_ticks == 1U, + "zero-delay recovery reaches a fresh dispatch"); + + setup(2U, 2U); + g_runtime[1].state = WT_GUEST_FAULTED; + event = fault_current(); + check(event == DISPATCHED && g_idle_ticks == 2U, + "terminal peer does not prevent recovery"); + + setup(2U, 2U); + g_runtime[1].state = WT_GUEST_STOPPED; + event = fault_current(); + check(event == DISPATCHED && g_idle_ticks == 2U, + "stopped peer does not prevent recovery"); + + setup(2U, 2U); + g_runtime[1].state = WT_GUEST_RESTARTING; + g_runtime[1].remaining_delay_ticks = 1U; + event = fault_current(); + check(event == DISPATCHED && g_scheduler.current_guest == 1U && + g_idle_ticks == 1U && g_runtime[0].state == WT_GUEST_RESTARTING, + "earliest completed recovery is dispatched"); + + setup(2U, 2U); + g_runtime[1].state = WT_GUEST_READY; + event = fault_current(); + check(event == DISPATCHED && g_scheduler.current_guest == 1U && + g_idle_entries == 0U && g_idle_ticks == 0U, + "runnable peer retains immediate scheduling"); + + setup(1U, 2U); + g_runtime[0].restart_count = 3U; + event = fault_current(); + check(event == HALTED && g_runtime[0].state == WT_GUEST_FAULTED && + g_idle_entries == 0U, + "exhausted recovery budget remains terminal"); + + setup(2U, 0U); + g_runtime[0].state = WT_GUEST_STOPPED; + g_runtime[1].state = WT_GUEST_FAULTED; + check(schedule() == HALTED && g_idle_entries == 0U, + "terminal lifecycle states halt without idling"); + + setup(1U, 1U); + g_handler = false; + g_runtime[0].state = WT_GUEST_RESTARTING; + g_runtime[0].remaining_delay_ticks = 1U; + check(schedule() == DISPATCHED && g_idle_entries == 0U && + g_idle_ticks == 1U, "thread-mode recovery uses the existing idle stack"); + + setup(0U, 0U); + check(schedule() == HALTED, "empty scheduler remains terminal"); + + setup(1U, 1U); + g_configs[0].launch_required = 1U; + g_configs[0].memory_window_count = 1U; + g_configs[0].memory_windows[0].attributes = WT_MEM_ATTR_EXEC; + g_verify_result = WT_GUEST_VERIFY_ERROR_DIGEST; + event = fault_current(); + check(event == HALTED && g_runtime[0].state == WT_GUEST_FAULTED && + g_resets == 0U, "recovery verification failure remains terminal"); + + return g_failures == 0 ? 0 : 1; +} diff --git a/tests/target/lib/scenario_matrix.py b/tests/target/lib/scenario_matrix.py index bb4e0616f..1e1ed5020 100755 --- a/tests/target/lib/scenario_matrix.py +++ b/tests/target/lib/scenario_matrix.py @@ -55,7 +55,7 @@ ("positive", "Positive lifecycle"), ("bothpsa", "Both-OS PSA parity (Zephyr + FreeRTOS)"), ("bothiso", "Both-OS FF-M isolation negatives"), - ("restart", "Guest restart recovery"), + ("restart restartidle", "Guest restart recovery and idle backoff"), ("crossdomain", "Cross-domain isolation (L3)"), ("keystoreneg", "Keystore-band isolation (L3)"), ("deputyneg", "Keystore-flash privileged-deputy refused (L3)"), diff --git a/tests/target/run_m33mu_scenario.sh b/tests/target/run_m33mu_scenario.sh index 3583f0b28..280cc4487 100755 --- a/tests/target/run_m33mu_scenario.sh +++ b/tests/target/run_m33mu_scenario.sh @@ -102,6 +102,9 @@ export WT_GUEST_RAM_SIZE=0x00010000 export WT_GUEST1_RAM_BASE=0x20010000 export WT_ZEPHYR_DTC_OVERLAY_FILE=boards/wolfboot-stm32h563.overlay export WT_MAX_GUESTS=2 +if [ "$scenario" = "restartidle" ]; then + export WT_MAX_GUESTS=1 +fi export ZEPHYR_BOARD=nucleo_h563zi/stm32h563xx/ns WOLFBOOT_REF=d85fa9dbdf6c36f47b7e96eba5c9df750ad3c963 M33MU_REF=f3c03675260264cdec815adebe4b020bb6fe57b8 @@ -169,7 +172,7 @@ cp "$repo/build/wolftrust.elf" "$repo/build/wolftrust-signed.elf" # --- Guests. The restart scenario injects a Non-secure fault probe in the PSA # guest; the others build it unmodified. --- guest_flags="" -if [ "$scenario" = "restart" ]; then +if [ "$scenario" = "restart" ] || [ "$scenario" = "restartidle" ]; then guest_flags="WT_GUEST_FAULT_PROBE=1" elif [ "$scenario" = "confboot" ]; then guest_flags="WT_RUN_CONFORMANCE=1" @@ -280,6 +283,10 @@ fi if [ "$scenario" = "restart" ]; then quit_flag="" timeout_s=40 +elif [ "$scenario" = "restartidle" ]; then + quit_flag="" + expect_bkpt=0x7d + timeout_s=40 elif [ "$scenario" = "hsmfaultneg" ]; then quit_flag="" timeout_s=40 @@ -798,12 +805,18 @@ case "$scenario" in "wolfTrust FF-M conformance: val_entry start" echo "PASS: target/vaultrecoversec" ;; - restart) + restart|restartidle) expected=$((RESTART_LIMIT + 1)) banners=$(grep -c "guest0_psa alive" "$log" || true) if [ "$banners" -eq "$expected" ]; then check_pass "guest restarted $RESTART_LIMIT times then FAULTED (banners=$banners, expect $expected)" - echo "PASS: target/restart" + if [ "$scenario" = "restartidle" ]; then + if [ "$emu_status" -ne 0 ]; then + check_fail "terminal recovery verdict" "emulator status $emu_status" + fi + expect "terminal recovery verdict" "[EXPECT BKPT] Success" + fi + echo "PASS: target/$scenario" exit 0 fi check_fail "guest restart count" "saw $banners banners, expected $expected" From 8c6d5050e1c9bc88b27e070e1c329112005a38b5 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 23 Oct 2026 12:31:31 -0700 Subject: [PATCH 2/2] Preserve guest SysTick state across idle transitions --- docs/Testing.md | 4 + src/arch/armv8m/guest_context_armv8m.c | 21 +-- tests/firmware/systick-idle/main.c | 182 +++++++++++++++++++++++++ tests/firmware/systick-idle/test.ld | 42 ++++++ tests/target/run_m33mu_scenario.sh | 4 + tests/target/test_systick_idle.sh | 45 ++++++ 6 files changed, 289 insertions(+), 9 deletions(-) create mode 100644 tests/firmware/systick-idle/main.c create mode 100644 tests/firmware/systick-idle/test.ld create mode 100755 tests/target/test_systick_idle.sh diff --git a/docs/Testing.md b/docs/Testing.md index fa15ff783..01e1670a5 100644 --- a/docs/Testing.md +++ b/docs/Testing.md @@ -128,6 +128,10 @@ firmware update, manifest rejection, GTZC behavior, and VNET paths. `restartidle` runs the STM32H563 recovery check with `WT_MAX_GUESTS=1`. It requires every allowed relaunch and the final terminal verdict. The full tier runs it with both crypto engines. +It also runs `tests/target/test_systick_idle.sh`, which executes the production +Armv8-M timer code with modeled Non-secure timer registers and real Secure +SysTick interrupts. The check covers timer configuration and pending-tick +preservation across idle, repeated idle, resume, and interrupt ordering. Five scenarios cover processor-state isolation, and the emulator proves less than their names suggest: diff --git a/src/arch/armv8m/guest_context_armv8m.c b/src/arch/armv8m/guest_context_armv8m.c index e68d90d5a..146b26f83 100644 --- a/src/arch/armv8m/guest_context_armv8m.c +++ b/src/arch/armv8m/guest_context_armv8m.c @@ -284,15 +284,6 @@ void wt_arch_return_to_idle_thread( "b wt_arch_return_to_secure_thread \n"); } -void wt_arch_wait_for_interrupt(void) -{ - /* No Non-secure context owns the hardware timer while idle. */ - WT_SYST_NS_CSR = 0U; - WT_SCB_ICSR_NS = WT_SCB_ICSR_PENDSTCLR; - g_active_guest = UINT32_MAX; - __asm volatile("dsb 0xF\n wfi\n isb 0xF" ::: "memory"); -} - static void wt_exception_return_ns_msp(void) __attribute__((naked, noreturn)); static void wt_exception_return_ns_msp(void) @@ -508,6 +499,18 @@ static void wt_virtual_systick_save_departing(void) WT_SCB_ICSR_NS = WT_SCB_ICSR_PENDSTCLR; } +void wt_arch_wait_for_interrupt(void) +{ + uint32_t primask; + + /* Restart ticks must not interleave snapshot and ownership release. */ + __asm volatile("mrs %0, primask\n cpsid i" : "=r"(primask) :: "memory"); + wt_virtual_systick_save_departing(); + g_active_guest = UINT32_MAX; + __asm volatile("dsb 0xF\n wfi\n msr primask, %0\n isb 0xF" + :: "r"(primask) : "memory"); +} + static void wt_virtual_systick_account_elapsed(wt_virtual_systick_t* systick) { uint64_t elapsed; diff --git a/tests/firmware/systick-idle/main.c b/tests/firmware/systick-idle/main.c new file mode 100644 index 000000000..cab5040ff --- /dev/null +++ b/tests/firmware/systick-idle/main.c @@ -0,0 +1,182 @@ +/* main.c + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfTrust. + * + * wolfTrust is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfTrust is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +#include "wolftrust/arch/armv8m/core_regs.h" + +static volatile uint32_t g_test_csr; +static volatile uint32_t g_test_rvr; +static volatile uint32_t g_test_cvr; +static volatile uint32_t g_test_icsr; +static volatile uint32_t g_test_ticks; +static volatile uint32_t g_test_inject; +static volatile uint32_t g_test_handoff; +static volatile uint32_t g_test_mask_seen; +static volatile uint32_t g_test_bad_handoff; +static volatile uint32_t g_test_reset_owner; + +static volatile uint32_t* test_reload_register(void); + +#undef WT_SYST_NS_CSR +#undef WT_SYST_NS_RVR +#undef WT_SYST_NS_CVR +#undef WT_SCB_ICSR_NS +#define WT_SYST_NS_CSR g_test_csr +#define WT_SYST_NS_RVR (*test_reload_register()) +#define WT_SYST_NS_CVR g_test_cvr +#define WT_SCB_ICSR_NS g_test_icsr + +/* Stable NS register model; Secure SysTick and WFI execute on the Cortex-M. */ +#include "../../../src/arch/armv8m/guest_context_armv8m.c" + +extern uint32_t _estack; +extern uint32_t _sbss; +extern uint32_t _ebss; +void Reset_Handler(void); + +__attribute__((section(".vectors"))) +const uintptr_t g_test_vectors[16] = { + [0] = (uintptr_t)&_estack, + [1] = (uintptr_t)Reset_Handler, + [3] = (uintptr_t)wt_platform_panic, + [15] = (uintptr_t)SysTick_Handler +}; + +static volatile uint32_t* test_reload_register(void) +{ + if (g_test_inject != 0U) { + g_test_inject = 0U; + __asm volatile("mrs %0, primask" : "=r"(g_test_mask_seen)); + g_test_handoff = 1U; + WT_SCB_ICSR_S = WT_SCB_ICSR_PENDSTSET; + __asm volatile("dsb 0xF\n isb 0xF" ::: "memory"); + } + return &g_test_rvr; +} + +void wt_monitor_on_secure_timer(const wt_trap_frame_t* frame) +{ + (void)frame; + g_test_ticks++; + if (g_test_reset_owner != 0U) { + g_test_reset_owner = 0U; + wt_virtual_systick_reset(0U); + } + if (g_test_handoff != 0U && g_active_guest != UINT32_MAX) { + g_test_bad_handoff++; + } +} + +void wt_platform_panic(void) +{ + __asm volatile("bkpt #0x7e"); + for (;;) {} +} + +static void check(int ok) +{ + if (!ok) { + wt_platform_panic(); + } +} + +void Reset_Handler(void) +{ + uint32_t* p; + uint32_t primask; + wt_virtual_systick_t saved; + + for (p = &_sbss; p < &_ebss; ++p) { + *p = 0U; + } + wt_arch_init(); + g_systick_reload = 100000U; + g_secure_wall_cycles = 1000000U; + WT_SYST_RVR = g_systick_reload - 1U; + WT_SYST_CVR = 0U; + WT_SYST_CSR = WT_SYST_CSR_ENABLE | WT_SYST_CSR_TICKINT | + WT_SYST_CSR_CLKSOURCE; + + g_active_guest = 0U; + g_test_csr = WT_SYST_CSR_ENABLE | WT_SYST_CSR_TICKINT | + WT_SYST_CSR_CLKSOURCE; + g_test_rvr = 10000U; + g_test_cvr = 4321U; + g_test_icsr = WT_SCB_ICSR_PENDSTSET; + g_test_inject = 1U; + wt_arch_wait_for_interrupt(); + g_test_handoff = 0U; + check(g_guest_systick[0].csr == 7U && + g_guest_systick[0].rvr == 10000U && + g_guest_systick[0].cvr == 4321U && + g_guest_systick[0].pending == 1U && + g_guest_systick[0].owed_ticks == 1U); + check(g_active_guest == UINT32_MAX && g_test_csr == 0U && + g_test_icsr == WT_SCB_ICSR_PENDSTCLR); + __asm volatile("mrs %0, primask" : "=r"(primask)); + check(g_test_mask_seen == 1U && primask == 0U && + g_test_bad_handoff == 0U && g_test_ticks == 1U); + + saved = g_guest_systick[0]; + wt_arch_wait_for_interrupt(); + check(g_guest_systick[0].csr == saved.csr && + g_guest_systick[0].rvr == saved.rvr && + g_guest_systick[0].cvr == saved.cvr && + g_guest_systick[0].pending == saved.pending && + g_guest_systick[0].owed_ticks == saved.owed_ticks && + g_guest_systick[0].last_accounted_cycles == + saved.last_accounted_cycles); + + g_secure_wall_cycles = saved.last_accounted_cycles; + wt_virtual_systick_restore_arriving(0U); + check(g_arriving_systick_csr == saved.csr && + g_test_rvr == saved.cvr && g_arriving_systick_inject == 1U); + wt_virtual_systick_arm_arriving(); + check(g_test_csr == saved.csr && g_test_rvr == saved.rvr && + g_test_icsr == WT_SCB_ICSR_PENDSTSET); + + g_active_guest = 1U; + g_test_csr = WT_SYST_CSR_CLKSOURCE; + g_test_rvr = 321U; + g_test_cvr = 123U; + g_test_icsr = 0U; + wt_arch_wait_for_interrupt(); + check(g_guest_systick[1].csr == WT_SYST_CSR_CLKSOURCE && + g_guest_systick[1].rvr == 321U && + g_guest_systick[1].cvr == 123U && + g_guest_systick[1].pending == 0U && + g_guest_systick[1].owed_ticks == 0U); + + /* A tick before the critical section completes before ownership is read. */ + g_active_guest = 0U; + g_test_csr = 7U; + g_test_reset_owner = 1U; + WT_SCB_ICSR_S = WT_SCB_ICSR_PENDSTSET; + __asm volatile("dsb 0xF\n isb 0xF" ::: "memory"); + check(g_test_reset_owner == 0U); + wt_arch_wait_for_interrupt(); + check(g_active_guest == UINT32_MAX && + g_guest_systick[0].csr == 0U && + g_guest_systick[0].rvr == 0U && + g_guest_systick[0].cvr == 0U && + g_guest_systick[0].pending == 0U && + g_guest_systick[0].owed_ticks == 0U); + __asm volatile("bkpt #0x7f"); + for (;;) {} +} diff --git a/tests/firmware/systick-idle/test.ld b/tests/firmware/systick-idle/test.ld new file mode 100644 index 000000000..a309247ee --- /dev/null +++ b/tests/firmware/systick-idle/test.ld @@ -0,0 +1,42 @@ +/* test.ld + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfTrust. + * + * wolfTrust is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfTrust is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +ENTRY(Reset_Handler) +MEMORY +{ + FLASH (rx) : ORIGIN = 0x0C000000, LENGTH = 128K + RAM (rwx) : ORIGIN = 0x30000000, LENGTH = 64K +} +_estack = ORIGIN(RAM) + LENGTH(RAM); +SECTIONS +{ + .text : { + KEEP(*(.vectors)) + *(.text*) + *(.rodata*) + } > FLASH + .data : { *(.data*) } > RAM AT > FLASH + .bss (NOLOAD) : { + _sbss = .; + *(.bss*) + *(COMMON) + _ebss = .; + } > RAM +} diff --git a/tests/target/run_m33mu_scenario.sh b/tests/target/run_m33mu_scenario.sh index 280cc4487..baae71213 100755 --- a/tests/target/run_m33mu_scenario.sh +++ b/tests/target/run_m33mu_scenario.sh @@ -129,6 +129,10 @@ else cd "$repo" fi +if [ "$scenario" = "restartidle" ]; then + M33MU="$M33MU" bash tests/target/test_systick_idle.sh +fi + # --- wolfTrust dependencies + a clean secure build (drop any stale build/ # whose generated wolfhsm_cfg.h bakes in a host-absolute path). The guest # CMake cache must go too: scenarios configure guest0_psa with different diff --git a/tests/target/test_systick_idle.sh b/tests/target/test_systick_idle.sh new file mode 100755 index 000000000..43fb259c6 --- /dev/null +++ b/tests/target/test_systick_idle.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# test_systick_idle.sh +# +# Copyright (C) 2026 wolfSSL Inc. +# +# This file is part of wolfTrust. +# +# wolfTrust is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# wolfTrust is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, see . + + +set -euo pipefail +repo="$(cd "$(dirname "$0")/../.." && pwd)" +build="${WT_SYSTICK_TEST_BUILD:-$repo/build/systick-idle}" +prefix="${TOOLPREFIX:-arm-none-eabi-}" +emulator="${M33MU:-m33mu}" +mkdir -p "$build" +build="$(cd "$build" && pwd)" +if [[ "$emulator" == */* && "$emulator" != /* ]]; then + emulator="$PWD/$emulator" +fi +"${prefix}gcc" -mcpu=cortex-m33 -mthumb -mgeneral-regs-only -mcmse \ + -std=c99 -ffreestanding -fno-builtin -nostdlib -Os -g \ + -ffunction-sections -fdata-sections -Wall -Wextra \ + -I"$repo/include" -I"$repo/port/stm32h563" \ + -DWT_MAX_GUESTS=2 -DWT_TARGET_BUILD=1 \ + -Wl,--gc-sections -Wl,-T,"$repo/tests/firmware/systick-idle/test.ld" \ + -o "$build/test.elf" "$repo/tests/firmware/systick-idle/main.c" -lgcc +"${prefix}objcopy" -O binary "$build/test.elf" "$build/test.bin" +echo 'RUN: virtual SysTick idle preservation' +(cd "$build"; "$emulator" "$build/test.bin" --expect-bkpt 0x7f --timeout 10) \ + | tee "$build/test.log" +grep -Fq '[EXPECT BKPT] Success' "$build/test.log" +echo 'PASS: virtual SysTick idle snapshot, repeat idle, resume, and interrupt ordering' +echo 'PASS: pre-entry restart tick does not resurrect timer state'