diff --git a/docs/Architecture.md b/docs/Architecture.md
index 697ace94..8e77e18f 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 66103458..8f92fd5f 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 c35de758..01e1670a 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,14 @@ 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.
+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/include/wolftrust/arch.h b/include/wolftrust/arch.h
index 84f1a46a..c1893791 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 2d3fa26d..146b26f8 100644
--- a/src/arch/armv8m/guest_context_armv8m.c
+++ b/src/arch/armv8m/guest_context_armv8m.c
@@ -272,6 +272,18 @@ 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");
+}
+
static void wt_exception_return_ns_msp(void) __attribute__((naked, noreturn));
static void wt_exception_return_ns_msp(void)
@@ -487,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/src/monitor.c b/src/monitor.c
index 01f88dd9..520a1581 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/firmware/systick-idle/main.c b/tests/firmware/systick-idle/main.c
new file mode 100644
index 00000000..cab5040f
--- /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 00000000..a309247e
--- /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/host/Makefile b/tests/host/Makefile
index 4e82e96a..cf86f5ba 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 00000000..13a97260
--- /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 00000000..9d897344
--- /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 bb4e0616..1e1ed502 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 3583f0b2..baae7121 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
@@ -126,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
@@ -169,7 +176,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 +287,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 +809,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"
diff --git a/tests/target/test_systick_idle.sh b/tests/target/test_systick_idle.sh
new file mode 100755
index 00000000..43fb259c
--- /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'