From 4924cb26f10451e88f5fa28d2d6b8c6b1a8e48c7 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 23 Oct 2026 21:46:26 +0100 Subject: [PATCH 01/45] Let a port carry its own secure owner map entries --- docs/Porting.md | 7 +++++-- mk/arch-armv8m.mk | 13 +++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/docs/Porting.md b/docs/Porting.md index 0374af00..633fed6b 100644 --- a/docs/Porting.md +++ b/docs/Porting.md @@ -118,7 +118,9 @@ A port supplies: the conformance sections, .data and .bss); - attribution read-back for every Secure peripheral the SPM uses, panicking at boot on a mismatch; and -- owner lines in `tools/secure_owners.txt` for every object it adds. +- owner lines for every object it adds, in `tools/secure_owners.txt` or in + the port's own `port//secure_owners.txt`, which the secure link + appends to the shared map. The build reads `WT_RAM_S_BASE` and the band placement for the linker and derives the post-link band check from `memory_map.h`, so no band address is @@ -237,7 +239,8 @@ worked examples above give a concrete map for each board. Makefile selects them from `ARCH` and `TARGET`, and `mk/common.mk` needs no change. 4. Supply startup/vector and linker handling appropriate to the target, and - give every object the port links an owner in `tools/secure_owners.txt`. + give every object the port links an owner in `tools/secure_owners.txt` + or in `port//secure_owners.txt`. 5. Generate the manifest at build time and include its digest in the signed Secure image. 6. Integrate application domains with the architecture's client boundary and diff --git a/mk/arch-armv8m.mk b/mk/arch-armv8m.mk index 31345681..22a9e243 100644 --- a/mk/arch-armv8m.mk +++ b/mk/arch-armv8m.mk @@ -101,10 +101,19 @@ ARCH_LINK_OUTPUTS := $(SECURE_CMSE_IMPLIB) ARCH_LDFLAGS = -Wl,--cmse-implib -Wl,--out-implib=$(SECURE_CMSE_IMPLIB) \ $(WT_L3_LINK_ARGS) +# The owner map the post-link layout check reads: the shared map plus the +# port's own objects (port//secure_owners.txt), so a port adds its +# owners without editing the shared file. +WT_SECURE_OWNERS := $(BUILD_DIR)/secure_owners.txt +$(WT_SECURE_OWNERS): $(ROOT)/tools/secure_owners.txt \ + $(wildcard $(PORT_DIR)/secure_owners.txt) | $(BUILD_DIR) + cat $^ > $@ + # A changed post-link checker must relink so the image is checked again. $(BUILD_DIR)/wolftrust.elf $(ARCH_LINK_OUTPUTS): \ $(ROOT)/tools/check_no_fp_insn.py $(ROOT)/tools/check_stack_seal.py \ - $(ROOT)/tools/l3_layout_args.py $(wildcard $(PORT_COMMON_DIR)/*.ld) + $(ROOT)/tools/l3_layout_args.py $(wildcard $(PORT_COMMON_DIR)/*.ld) \ + $(WT_SECURE_OWNERS) # Whitelist of non-secure-callable veneers the linked secure image may # export: exactly the five mediated FF-M gateway entries, pinned by full @@ -129,7 +138,7 @@ define arch_image_checks fi @python3 $(ROOT)/tools/check_secure_layout.py \ --nm $(TOOLPREFIX)nm $(WT_SECURE_LAYOUT_ARGS) $(SECURE_ELF) \ - --map $(SECURE_MAP) --owners $(ROOT)/tools/secure_owners.txt \ + --map $(SECURE_MAP) --owners $(WT_SECURE_OWNERS) \ $(if $(filter 1,$(WT_CONFORMANCE)),,--production) \ --objects-nm $(TOOLPREFIX)gcc-nm --objects $(ALL_SECURE_OBJS) @if grep -E ' (malloc|free|calloc|realloc|_sbrk|_malloc_r|_free_r)$$' \ From 84876241d1211abc18b83345a019d6db2c6fe2fa Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 23 Oct 2026 21:46:26 +0100 Subject: [PATCH 02/45] Add the MCXN947 port --- mk/target-mcxn947.mk | 68 ++ port/mcxn947/conformance/conf_nvm.h | 39 + port/mcxn947/conformance/conf_nvm_sync.c | 79 ++ .../conformance/pal_attestation_config.h | 58 ++ port/mcxn947/conformance/pal_config.h | 115 +++ port/mcxn947/conformance/pal_crypto_config.h | 70 ++ port/mcxn947/conformance/pal_driver_intf.c | 154 ++++ port/mcxn947/hsm_flash.c | 683 ++++++++++++++++ port/mcxn947/hsm_flash.h | 53 ++ port/mcxn947/hsm_flash_ctx.c | 27 + port/mcxn947/l3_port.h | 31 + port/mcxn947/manifest-conformance.json | 728 ++++++++++++++++++ port/mcxn947/manifest.json | 501 ++++++++++++ port/mcxn947/mcxn947_regs.h | 70 ++ port/mcxn947/memory_map.h | 108 +++ port/mcxn947/partitions.c | 504 ++++++++++++ port/mcxn947/platform_mcxn947.c | 363 +++++++++ port/mcxn947/rng_entropy.c | 144 ++++ port/mcxn947/rom_flash.c | 251 ++++++ port/mcxn947/rom_flash.h | 41 + port/mcxn947/secure.ld | 134 ++++ port/mcxn947/secure_owners.txt | 3 + 22 files changed, 4224 insertions(+) create mode 100644 mk/target-mcxn947.mk create mode 100644 port/mcxn947/conformance/conf_nvm.h create mode 100644 port/mcxn947/conformance/conf_nvm_sync.c create mode 100644 port/mcxn947/conformance/pal_attestation_config.h create mode 100644 port/mcxn947/conformance/pal_config.h create mode 100644 port/mcxn947/conformance/pal_crypto_config.h create mode 100644 port/mcxn947/conformance/pal_driver_intf.c create mode 100644 port/mcxn947/hsm_flash.c create mode 100644 port/mcxn947/hsm_flash.h create mode 100644 port/mcxn947/hsm_flash_ctx.c create mode 100644 port/mcxn947/l3_port.h create mode 100644 port/mcxn947/manifest-conformance.json create mode 100644 port/mcxn947/manifest.json create mode 100644 port/mcxn947/mcxn947_regs.h create mode 100644 port/mcxn947/memory_map.h create mode 100644 port/mcxn947/partitions.c create mode 100644 port/mcxn947/platform_mcxn947.c create mode 100644 port/mcxn947/rng_entropy.c create mode 100644 port/mcxn947/rom_flash.c create mode 100644 port/mcxn947/rom_flash.h create mode 100644 port/mcxn947/secure.ld create mode 100644 port/mcxn947/secure_owners.txt diff --git a/mk/target-mcxn947.mk b/mk/target-mcxn947.mk new file mode 100644 index 00000000..03053990 --- /dev/null +++ b/mk/target-mcxn947.mk @@ -0,0 +1,68 @@ +# NXP MCXN947 (FRDM-MCXN947, Cortex-M33 CPU0) target inputs for the secure +# image build. Included first, before mk/arch-.mk and mk/common.mk; +# repository paths come from the Makefile. +WT_CPU ?= cortex-m33 +PORT_DIR := $(ROOT)/port/mcxn947 +PORT_HEADERS := $(wildcard $(PORT_DIR)/*.h) +TARGET_CONF_DIR := $(PORT_DIR)/conformance + +# WT_CONFORMANCE=1 swaps in the manifest that also hosts Arm's test partitions. +ifeq ($(CONFIG_VNET),y) +$(error TARGET=mcxn947 has no SERVICE_VNET manifest; build with CONFIG_VNET=n) +endif +ifeq ($(WT_CONFORMANCE),1) +MANIFEST_INPUT := $(PORT_DIR)/manifest-conformance.json +else +MANIFEST_INPUT := $(PORT_DIR)/manifest.json +endif + +# Guests share LPUART4 (LP_FLEXCOMM4), the FRDM board's MCU-Link VCOM console. +WT_SHARED_UART ?= 0 +# Clocks left as the first loader configured them (PLL150M core, FRO12M on +# FLEXCOMM4); verify against the RM before a guest derives a baud divider. +WT_GUEST_CORE_CLOCK_HZ ?= 150000000 +WT_GUEST_UART_CLOCK_HZ ?= 12000000 +# The conformance PAL interrupt source: LP_FLEXCOMM1's NVIC line, raised by +# software so no peripheral needs bringing up for it. +WT_CONF_IRQ ?= 36 +WT_CONF_IRQ_HANDLER ?= LP_FLEXCOMM1_IRQHandler + +# Internal flash through its Secure alias. The default is the wolfBoot +# mcxn-tz-wolftrust layout (wolfBoot at flash+0, BOOT at 0x40000). +WT_SECURE_FLASH_BASE ?= 0x10040000 +WT_SECURE_FLASH_SIZE ?= 0x00040000 +# wolfBoot's MCXN image header (IMAGE_HEADER_SIZE=1024) precedes the vectors. +WT_SECURE_IMAGE_HEADER_SIZE ?= 0x400 +WT_GUEST0_FLASH_BASE ?= 0x00080000 +WT_GUEST1_FLASH_BASE ?= 0x00100000 +WT_GUEST0_FLASH_SIZE ?= 0x00080000 +WT_GUEST1_FLASH_SIZE ?= 0x00040000 + +TARGET_CFLAGS := \ + -DWT_SHARED_UART=$(WT_SHARED_UART) \ + -DWT_GUEST_CORE_CLOCK_HZ=$(WT_GUEST_CORE_CLOCK_HZ) \ + -DWT_GUEST_UART_CLOCK_HZ=$(WT_GUEST_UART_CLOCK_HZ) \ + -DWT_CONF_IRQ=$(WT_CONF_IRQ)u \ + -DWT_CONF_IRQ_HANDLER=$(WT_CONF_IRQ_HANDLER) \ + -DWT_SECURE_FLASH_BASE=$(WT_SECURE_FLASH_BASE) \ + -DWT_SECURE_FLASH_SIZE=$(WT_SECURE_FLASH_SIZE) \ + -DWT_SECURE_IMAGE_HEADER_SIZE=$(WT_SECURE_IMAGE_HEADER_SIZE) \ + -DWT_GUEST0_FLASH_BASE=$(WT_GUEST0_FLASH_BASE) \ + -DWT_GUEST1_FLASH_BASE=$(WT_GUEST1_FLASH_BASE) \ + -DWT_GUEST0_FLASH_SIZE=$(WT_GUEST0_FLASH_SIZE) \ + -DWT_GUEST1_FLASH_SIZE=$(WT_GUEST1_FLASH_SIZE) +TARGET_LDFLAGS := \ + -Wl,--defsym=WT_SECURE_FLASH_ORIGIN=$(WT_SECURE_FLASH_BASE) \ + -Wl,--defsym=WT_SECURE_FLASH_SIZE=$(WT_SECURE_FLASH_SIZE) \ + -Wl,--defsym=WT_SECURE_IMAGE_HEADER_SIZE=$(WT_SECURE_IMAGE_HEADER_SIZE) +SECURE_LD := $(PORT_DIR)/secure.ld +# This port links no wolfHAL, so the post-link check expects no wolfHAL state. +WT_SECURE_LAYOUT_EXTRA_ARGS := --no-wolfhal + +TARGET_PLATFORM_SRC := $(PORT_DIR)/platform_mcxn947.c +TARGET_PARTITIONS_SRC := $(PORT_DIR)/partitions.c +TARGET_EXTRA_SRCS := \ + $(wildcard $(PORT_DIR)/rng_entropy.c) \ + $(wildcard $(PORT_DIR)/rom_flash.c) \ + $(wildcard $(PORT_DIR)/hsm_flash.c) \ + $(wildcard $(PORT_DIR)/hsm_flash_ctx.c) diff --git a/port/mcxn947/conformance/conf_nvm.h b/port/mcxn947/conformance/conf_nvm.h new file mode 100644 index 00000000..c126d477 --- /dev/null +++ b/port/mcxn947/conformance/conf_nvm.h @@ -0,0 +1,39 @@ +/* conf_nvm.h + * + * 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 . + */ + +#ifndef WOLFTRUST_MCXN947_CONF_NVM_H +#define WOLFTRUST_MCXN947_CONF_NVM_H + +#include + +/* Survive-reset NVM seam for the conformance DRIVER partition (P5 K2). The + * unprivileged PAL calls this to persist its shadow: store==0 loads the flash + * sector into buf, store!=0 writes buf back. Returns 0 on success. The target + * build issues an SVC to the privileged flash driver; a host test links its + * own flash-simulator implementation of this symbol instead. */ +int wt_conf_nvm_sync(uint8_t *buf, uint32_t len, int store); + +/* PAL interrupt source control (P4.2c): on!=0 drives the platform UART so its + * NVIC line fires into the DRIVER partition's manifest interrupt signal; on==0 + * quiesces the source. The unprivileged PAL traps to the privileged device + * poke via SVC; the interrupt itself is delivered through the real vector. */ +int wt_conf_irq_set(int on); + +#endif /* WOLFTRUST_MCXN947_CONF_NVM_H */ diff --git a/port/mcxn947/conformance/conf_nvm_sync.c b/port/mcxn947/conformance/conf_nvm_sync.c new file mode 100644 index 00000000..4006423e --- /dev/null +++ b/port/mcxn947/conformance/conf_nvm_sync.c @@ -0,0 +1,79 @@ +/* conf_nvm_sync.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 . + */ + +/* Target side of the survive-reset NVM seam (P5 K2): trap the DRIVER + * partition's shadow buffer through the same SVC #1 the psa_* veneers use, so + * the privileged SPM can drive the flash controller. The host test provides a + * flash-simulator implementation of wt_conf_nvm_sync instead of this file. */ + +#include "conf_nvm.h" + +#include "wolftrust/ffm.h" +#include "wolftrust/spm_transport.h" +#include "wolftrust/spm_gate.h" + +#include "memory_map.h" +#include "pal_config.h" + +#include + +/* The scheduler carves these holes out of other partitions' domains using the + * memory_map.h constants; the Arm tests probe them via the pal_config.h ones. + * A drift between the two silently breaks the L3 MMIO-isolation panic tests. */ +#if SERVER_PARTITION_MMIO_0_START != WT_CONF_SERVER_MMIO_BASE || \ + SERVER_PARTITION_MMIO_0_END != (WT_CONF_SERVER_MMIO_BASE + \ + WT_CONF_SERVER_MMIO_SIZE) +#error "pal_config SERVER MMIO does not match memory_map WT_CONF_SERVER_MMIO" +#endif +#if DRIVER_PARTITION_MMIO_0_START != WT_CONF_DRV_MMIO_BASE || \ + DRIVER_PARTITION_MMIO_0_END != (WT_CONF_DRV_MMIO_BASE + \ + WT_CONF_DRV_MMIO_SIZE) +#error "pal_config DRIVER MMIO does not match memory_map WT_CONF_DRV_MMIO" +#endif + +int wt_conf_nvm_sync(uint8_t *buf, uint32_t len, int store) +{ + wt_spm_call_t call; + + (void)memset(&call, 0, sizeof(call)); + call.op = WT_SPM_OP_CONF_NVM_SYNC; + call.buffer = buf; + call.num_bytes = (size_t)len; + call.call_type = store; + + if (wt_spm_sp_call(&call) != WT_FFM_SUCCESS) { + return -1; + } + return call.ret_int; +} + +int wt_conf_irq_set(int on) +{ + wt_spm_call_t call; + + (void)memset(&call, 0, sizeof(call)); + call.op = WT_SPM_OP_CONF_IRQ_SET; + call.call_type = on; + + if (wt_spm_sp_call(&call) != WT_FFM_SUCCESS) { + return -1; + } + return call.ret_int; +} diff --git a/port/mcxn947/conformance/pal_attestation_config.h b/port/mcxn947/conformance/pal_attestation_config.h new file mode 100644 index 00000000..7c147f75 --- /dev/null +++ b/port/mcxn947/conformance/pal_attestation_config.h @@ -0,0 +1,58 @@ +/* pal_attestation_config.h + * + * 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 . + */ + +/* Attestation PAL configuration for the unmodified Arm psa-arch-tests + * dev_apis/initial_attestation suite. PLATFORM_OVERRIDE_ATTEST_PK stays + * undefined: the wolfTrust IAK is generated inside the wolfHSM vault per + * device, so the verify key must be fetched at runtime through + * tfm_initial_attest_get_public_key (conformance_pal.c) rather than + * baked in as upstream's well-known TF-M test key. */ + +#ifndef _PAL_ATTESTATION_CONFIG_H_ +#define _PAL_ATTESTATION_CONFIG_H_ + +#include +#include + +#define CRYPTO_VERSION_BETA3 + +#define COSE_ALGORITHM_ES256 -7 +#define COSE_ALG_SHA256_PROPRIETARY -72000 + +#define USEFUL_BUF_MAKE_STACK_UB UsefulBuf_MAKE_STACK_UB + +#define COSE_SIG_CONTEXT_STRING_SIGNATURE1 "Signature1" + +#define T_COSE_SIGN1_MAX_PROT_HEADER (1 + 1 + 5 + 9) + +#define T_COSE_SIZE_OF_TBS \ + (1 + sizeof(COSE_SIG_CONTEXT_STRING_SIGNATURE1) + 2 + \ + T_COSE_SIGN1_MAX_PROT_HEADER + 3) + +#define NULL_USEFUL_BUF_C NULLUsefulBufC + +#define ECC_CURVE_SECP256R1_PULBIC_KEY_LENGTH (1 + 2 * 32) + +int32_t tfm_initial_attest_get_public_key(uint8_t *public_key_buff, + size_t public_key_buf_size, + size_t *public_key_len, + psa_ecc_family_t *elliptic_family_type); + +#endif /* _PAL_ATTESTATION_CONFIG_H_ */ diff --git a/port/mcxn947/conformance/pal_config.h b/port/mcxn947/conformance/pal_config.h new file mode 100644 index 00000000..38ff66be --- /dev/null +++ b/port/mcxn947/conformance/pal_config.h @@ -0,0 +1,115 @@ +/* pal_config.h + * + * 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 . + */ + +/* Arm psa-arch-tests PAL target configuration for wolfTrust on MCXN947. + * P3a supplies the compile surface the partition sources include; the driver + * plane (P3b) binds the UART/WD/NVMEM values to real secure devices and the + * isolation MMIO windows (P4) to enforced regions. */ + +#ifndef _PAL_CONFIG_H_ +#define _PAL_CONFIG_H_ + +#define PLATFORM_PSA_ISOLATION_LEVEL 3 + +#define UART_NUM 1 +#define UART_0_BASE 0x500B4000 +#define UART_0_SIZE 0x3FF +#define UART_0_INTR_ID 0xFF +#define UART_0_PERMISSION TYPE_READ_WRITE + +#define WATCHDOG_NUM 1 +#define WATCHDOG_0_BASE 0x50016000 +#define WATCHDOG_0_SIZE 0x3FF +#define WATCHDOG_0_INTR_ID 0xFF +#define WATCHDOG_0_PERMISSION TYPE_READ_WRITE +#define WATCHDOG_0_NUM_OF_TICK_PER_MICRO_SEC 0x3 +#define WATCHDOG_0_TIMEOUT_IN_MICRO_SEC_LOW 0xF4240 +#define WATCHDOG_0_TIMEOUT_IN_MICRO_SEC_MEDIUM 0x1E8480 +#define WATCHDOG_0_TIMEOUT_IN_MICRO_SEC_HIGH 0x4C4B40 +#define WATCHDOG_0_TIMEOUT_IN_MICRO_SEC_CRYPTO 0x1312D00 + +#define NVMEM_NUM 1 +#define NVMEM_0_START 0x101E4000 +#define NVMEM_0_END 0x101E43FF +#define NVMEM_0_PERMISSION TYPE_READ_WRITE + +#define NSPE_MMIO_NUM 1 +#define NSPE_MMIO_0_START 0x20041F00 +#define NSPE_MMIO_0_END 0x20041F1F +#define NSPE_MMIO_0_PERMISSION TYPE_READ_WRITE + +/* Per-partition pseudo-MMIO holes carved from the top of the shared CONFDATA + * window, above the conformance .data/.bss fill. MUST match the + * WT_CONF_*_MMIO_* constants in port/mcxn947/memory_map.h (an #error + * cross-check in conf_nvm_sync.c enforces it). */ +#define SERVER_PARTITION_MMIO_NUM 1 +#define SERVER_PARTITION_MMIO_0_START 0x3005DC00 +#define SERVER_PARTITION_MMIO_0_END 0x3005DD00 +#define SERVER_PARTITION_MMIO_0_PERMISSION TYPE_READ_WRITE + +#define DRIVER_PARTITION_MMIO_NUM 1 +#define DRIVER_PARTITION_MMIO_0_START 0x3005DE00 +#define DRIVER_PARTITION_MMIO_0_END 0x3005DF00 +#define DRIVER_PARTITION_MMIO_0_PERMISSION TYPE_READ_WRITE + +#define PLATFORM_WD_BASE WATCHDOG_0_BASE +#define PLATFORM_WD_NUM_OF_TICK_PER_MICRO_SEC WATCHDOG_0_NUM_OF_TICK_PER_MICRO_SEC +#define PLATFORM_WD_TIMEOUT_IN_MICRO_SEC_LOW WATCHDOG_0_TIMEOUT_IN_MICRO_SEC_LOW +#define PLATFORM_WD_TIMEOUT_IN_MICRO_SEC_MEDIUM WATCHDOG_0_TIMEOUT_IN_MICRO_SEC_MEDIUM +#define PLATFORM_WD_TIMEOUT_IN_MICRO_SEC_HIGH WATCHDOG_0_TIMEOUT_IN_MICRO_SEC_HIGH +#define PLATFORM_WD_TIMEOUT_IN_MICRO_SEC_CRYPTO WATCHDOG_0_TIMEOUT_IN_MICRO_SEC_CRYPTO + +#define PLATFORM_NVM_BASE NVMEM_0_START + +#define PLATFORM_NSPE_MMIO_START NSPE_MMIO_0_START +#define PLATFORM_SERVER_PARTITION_MMIO_START SERVER_PARTITION_MMIO_0_START +#define PLATFORM_DRIVER_PARTITION_MMIO_START DRIVER_PARTITION_MMIO_0_START +#define PLATFORM_DRIVER_PARTITION_MMIO_END DRIVER_PARTITION_MMIO_0_END + +#ifdef IPC +#include "psa/client.h" +#include "psa_manifest/sid.h" +#include "psa_manifest/pid.h" +#endif + +/* dev_apis Storage (P4-S6): the val NSPE and test TUs reach the PSA storage + * types and API version macros through this per-target config, mirroring the + * upstream tgt_dev_apis targets. */ +#if defined(STORAGE) || defined(INTERNAL_TRUSTED_STORAGE) || \ + defined(PROTECTED_STORAGE) +#include "psa/internal_trusted_storage.h" +#include "psa/protected_storage.h" +#define ARCH_TEST_STORAGE_UID_MAX_SIZE 512 +#endif + +/* dev_apis Crypto (P4-S6): wolfPSA is the guest's psa_* provider; the + * algorithm surface the suite may exercise lives in pal_crypto_config.h. */ +#if defined(CRYPTO) +#include "psa/crypto.h" +#include "pal_crypto_config.h" +#endif + +#if defined(INITIAL_ATTESTATION) +#include "psa/crypto.h" +#include "psa/initial_attestation.h" +#include "pal_attestation_config.h" +#endif + +#endif /* _PAL_CONFIG_H_ */ diff --git a/port/mcxn947/conformance/pal_crypto_config.h b/port/mcxn947/conformance/pal_crypto_config.h new file mode 100644 index 00000000..c75d2ca0 --- /dev/null +++ b/port/mcxn947/conformance/pal_crypto_config.h @@ -0,0 +1,70 @@ +/** @file + * Copyright (c) 2019-2025, Arm Limited or its affiliates. All rights reserved. + * SPDX-License-Identifier : Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +**/ + +/* Crypto test configuration for the wolfTrust MCXN947 guest, derived from + * the Arm pal_crypto_config.h template. The enabled set mirrors exactly what + * the guest wolfPSA/wolfCrypt build compiles in (module/wolfhsm-client + * user_settings.h): SHA-256, HMAC, HKDF, PBKDF2, TLS-1.2 PRF, AES + * CBC/CTR/GCM/CCM, ECC P-256 ECDSA/ECDH incl. RFC 6979. Everything absent + * from that build (RSA, DES, ChaCha20, CMAC, SHA-1/224/384/512, PAKE) stays + * off so the suite skips those vectors instead of failing on honest + * NOT_SUPPORTED. */ + +#ifndef _PAL_CRYPTO_CONFIG_H_ +#define _PAL_CRYPTO_CONFIG_H_ + +#define ARCH_TEST_RAW + +#define ARCH_TEST_AES +#define ARCH_TEST_AES_128 +#define ARCH_TEST_AES_192 +#define ARCH_TEST_AES_256 +#define ARCH_TEST_AES_512 + +#define ARCH_TEST_CIPHER +#define ARCH_TEST_CIPHER_MODE_CTR +#define ARCH_TEST_CIPHER_MODE_CBC +#define ARCH_TEST_CTR_AES +#define ARCH_TEST_CBC_AES +#define ARCH_TEST_CBC_AES_NO_PADDING +#define ARCH_TEST_CBC_NO_PADDING +#define ARCH_TEST_CBC_PKCS7 + +#define ARCH_TEST_HASH +#define ARCH_TEST_SHA256 + +#define ARCH_TEST_CCM +#define ARCH_TEST_GCM + +#define ARCH_TEST_HMAC +#define ARCH_TEST_HKDF +#define ARCH_TEST_HKDF_EXTRACT +#define ARCH_TEST_HKDF_EXPAND +#define ARCH_TEST_PBKDF2 +#define ARCH_TEST_TLS12_PRF +#define ARCH_TEST_TLS12_PSK_TO_MS +#define ARCH_TEST_TRUNCATED_MAC + +#define ARCH_TEST_ECC +#define ARCH_TEST_ECC_CURVE_SECP256R1 +#define ARCH_TEST_ECDH +#define ARCH_TEST_ECDSA +#define ARCH_TEST_DETERMINISTIC_ECDSA + +#include "pal_crypto_config_check.h" + +#endif /* _PAL_CRYPTO_CONFIG_H_ */ diff --git a/port/mcxn947/conformance/pal_driver_intf.c b/port/mcxn947/conformance/pal_driver_intf.c new file mode 100644 index 00000000..7fe2a5f0 --- /dev/null +++ b/port/mcxn947/conformance/pal_driver_intf.c @@ -0,0 +1,154 @@ +/* pal_driver_intf.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 . + */ + +/* SPE PAL for the Arm psa-arch-tests DRIVER partition on wolfTrust (P3a). + * NVMEM is a flash-backed store surviving an AIRCR reset (P5 K2): a RAM shadow + * loaded from the reserved flash sector at first use and written through on + * every write via wt_conf_nvm_sync (SVC to the privileged flash driver). The + * watchdog and interrupt hooks are no-ops until P4/P6 provide the real + * devices; prints are swallowed until the secure UART routing lands in P3b. + * All state lives in the driver partition's own CONFDATA/.bss window. */ + +#include "conf_nvm.h" + +#include +#include +#include + +typedef uintptr_t addr_t; + +#define WT_CONF_DRV_NVM_SIZE 0x100u +static uint8_t g_drv_nvm[WT_CONF_DRV_NVM_SIZE]; +static uint8_t g_drv_nvm_ready; +static uint8_t g_drv_wd_enabled; + +static int wt_conf_drv_nvm_init(void) +{ + if (g_drv_nvm_ready == 0u) { + /* Reload the persisted contents; a blank sector reads back 0xFF, the + * same power-on state the RAM store used to fabricate. */ + if (wt_conf_nvm_sync(g_drv_nvm, sizeof(g_drv_nvm), 0) != 0) { + (void)memset(g_drv_nvm, 0xFF, sizeof(g_drv_nvm)); + } + g_drv_nvm_ready = 1u; + } + return 0; +} + +#if defined(WT_CONF_NVM_HOST_TEST) +/* Host-test seam: force the next access to reload from the flash simulator, + * modelling what a post-reset boot does. Never compiled into the target. */ +void wt_conf_drv_nvm_test_reset(void) +{ + g_drv_nvm_ready = 0u; +} +#endif + +void pal_uart_init(uint32_t uart_base_addr) +{ + (void)uart_base_addr; +} + +void pal_print_s(const char* str, int32_t data) +{ + (void)str; + (void)data; +} + +int pal_print(uint8_t c) +{ + (void)c; + return 0; +} + +int pal_nvmem_write(addr_t base, uint32_t offset, void* buffer, int size) +{ + (void)base; + (void)wt_conf_drv_nvm_init(); + /* Wrap-safe: offset + size overflows size_t on a 32-bit target, so + * compare each side against the array bound without adding them. */ + if (buffer == NULL || size < 0 || + (size_t)offset > sizeof(g_drv_nvm) || + (size_t)size > sizeof(g_drv_nvm) - (size_t)offset) { + return 0; + } + (void)memcpy(&g_drv_nvm[offset], buffer, (size_t)size); + /* Write through to flash so the value survives an AIRCR reset; a failed + * sync drops the shadow so the next access reloads what was persisted. */ + if (wt_conf_nvm_sync(g_drv_nvm, sizeof(g_drv_nvm), 1) != 0) { + g_drv_nvm_ready = 0u; + return 0; + } + return 1; +} + +int pal_nvmem_read(addr_t base, uint32_t offset, void* buffer, int size) +{ + (void)base; + (void)wt_conf_drv_nvm_init(); + /* Wrap-safe: offset + size overflows size_t on a 32-bit target, so + * compare each side against the array bound without adding them. */ + if (buffer == NULL || size < 0 || + (size_t)offset > sizeof(g_drv_nvm) || + (size_t)size > sizeof(g_drv_nvm) - (size_t)offset) { + return 0; + } + (void)memcpy(buffer, &g_drv_nvm[offset], (size_t)size); + return 1; +} + +int pal_wd_timer_init(addr_t base_addr, uint32_t time_us, + uint32_t timer_tick_us) +{ + (void)base_addr; + (void)time_us; + (void)timer_tick_us; + return 0; +} + +int pal_wd_timer_enable(addr_t base_addr) +{ + (void)base_addr; + g_drv_wd_enabled = 1u; + return 0; +} + +int pal_wd_timer_disable(addr_t base_addr) +{ + (void)base_addr; + g_drv_wd_enabled = 0u; + return 0; +} + +int pal_wd_timer_is_enabled(addr_t base_addr) +{ + (void)base_addr; + return (int)g_drv_wd_enabled; +} + +void pal_generate_interrupt(void) +{ + (void)wt_conf_irq_set(1); +} + +void pal_disable_interrupt(void) +{ + (void)wt_conf_irq_set(0); +} diff --git a/port/mcxn947/hsm_flash.c b/port/mcxn947/hsm_flash.c new file mode 100644 index 00000000..5fe42feb --- /dev/null +++ b/port/mcxn947/hsm_flash.c @@ -0,0 +1,683 @@ +/* hsm_flash.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 . + */ + +/* wolfHSM NVM backend on the MCXN947 internal flash. Reads go through the + * Secure alias; program and erase go through the boot ROM flash API + * (rom_flash.c), one 16-byte phrase at most once per erase. */ + +#include +#include +#include +#include + +#include "hsm_flash.h" +#include "memory_map.h" +#include "rom_flash.h" +#include "mcxn947_regs.h" +#include "wolftrust/arch.h" +#include "wolftrust/spm_transport.h" +#include "wolftrust/spm_gate.h" +#include "wolftrust/services/fwu_service.h" +#include "wolfhsm/wh_error.h" + +static const wt_hsm_flash_config_t g_hsm_flash_cfg = { + .base = WT_HSM_NVM_FLASH_BASE_S, + .size = WT_HSM_NVM_FLASH_SIZE, + .sector_size = WT_FLASH_SECTOR_SIZE, + .program_unit = WT_FLASH_PHRASE_SIZE, +}; + +#if defined(WT_CONFORMANCE) && (WT_CONFORMANCE == 1) +/* The conformance DRIVER partition's NVMEM boot flag lives in its own flash + * sector so it survives the suite's panic-test resets. */ +static const wt_hsm_flash_config_t g_conf_nvm_cfg = { + .base = WT_CONF_NVM_FLASH_BASE_S, + .size = WT_CONF_NVM_FLASH_SIZE, + .sector_size = WT_FLASH_SECTOR_SIZE, + .program_unit = WT_FLASH_PHRASE_SIZE, +}; +static wt_hsm_flash_context_t g_conf_nvm_ctx; +#endif +static const wt_hsm_flash_config_t g_fwu_flash_cfg = { + .base = WT_FWU_UPDATE_FLASH_BASE_S, + .size = WT_FWU_UPDATE_FLASH_SIZE, + .sector_size = WT_FLASH_SECTOR_SIZE, + .program_unit = WT_FLASH_PHRASE_SIZE, +}; +static wt_hsm_flash_context_t g_fwu_flash_ctx; +#if defined(WT_REMEASURE_PROBE) +static const wt_hsm_flash_config_t g_remeasure_probe_cfg = { + .base = WT_FLASH_TO_S_ALIAS(WT_GUEST0_FLASH_BASE), + .size = WT_FLASH_SECTOR_SIZE, + .sector_size = WT_FLASH_SECTOR_SIZE, + .program_unit = WT_FLASH_PHRASE_SIZE, +}; +static wt_hsm_flash_context_t g_remeasure_probe_ctx; +#endif +/* Last flash driver failure, read over the debug port by the harness. */ +volatile int32_t g_wt_flash_last_error __attribute__((used)); + +/* The context sits in the keystore band the confined partitions can write, so + * the privileged path takes its geometry from the const config it belongs to. */ +static const wt_hsm_flash_config_t *wt_hsm_flash_cfg_of(const void *context) +{ + if (context == (const void *)&g_hsm_flash_ctx) { + return &g_hsm_flash_cfg; + } +#if defined(WT_CONFORMANCE) && (WT_CONFORMANCE == 1) + if (context == (const void *)&g_conf_nvm_ctx) { + return &g_conf_nvm_cfg; + } +#endif + if (context == (const void *)&g_fwu_flash_ctx) { + return &g_fwu_flash_cfg; + } +#if defined(WT_REMEASURE_PROBE) + if (context == (const void *)&g_remeasure_probe_ctx) { + return &g_remeasure_probe_cfg; + } +#endif + return NULL; +} + +/* The store is read through the Secure alias; the ROM API takes the offset. */ +static uint32_t wt_hsm_flash_offset(const wt_hsm_flash_config_t *cfg, + uint32_t offset) +{ + return (uint32_t)(cfg->base - WT_FLASH_S_ALIAS_BASE) + offset; +} + +static int wt_hsm_flash_rom_result(int rc) +{ + if (rc == WT_ROM_FLASH_OK) { + return WH_ERROR_OK; + } + g_wt_flash_last_error = (int32_t)rc; + if (rc == WT_ROM_FLASH_ERR_NOTBLANK) { + return WH_ERROR_NOTBLANK; + } + if (rc == WT_ROM_FLASH_ERR_ARGS) { + return WH_ERROR_BADARGS; + } + return WH_ERROR_ABORTED; +} + +static int wt_flash_range_ok(const wt_hsm_flash_config_t *cfg, + uint32_t offset, uint32_t size) +{ + if (cfg == NULL || cfg->sector_size == 0u || cfg->program_unit == 0u) { + return 0; + } + if (offset > cfg->size) { + return 0; + } + if (size > cfg->size - offset) { + return 0; + } + return 1; +} + +/* A confined keystore partition hops to the privileged SVC dispatcher, which + * re-enters the same callback with privilege. */ +static int wt_hsm_flash_gate(void *context, int sub_op, uint32_t offset, + uint32_t size, void *data) +{ + wt_spm_call_t call; + int rc; + + if (context != (void *)&g_hsm_flash_ctx) { + return WH_ERROR_BADARGS; + } + (void)memset(&call, 0, sizeof(call)); + call.op = WT_SPM_OP_KEYSTORE_FLASH; + call.call_type = sub_op; + call.vec_idx = offset; + call.num_bytes = size; + call.buffer = data; + rc = wt_spm_sp_call(&call); + if (rc != WT_FFM_SUCCESS) { + g_wt_flash_gate_aborts++; + if (g_wt_flash_gate_abort_info == 0u) { + g_wt_flash_gate_abort_info = ((uint32_t)sub_op << 24) | + ((uint32_t)rc & 0x00FFFFFFu); + } + return WH_ERROR_ABORTED; + } + return call.ret_int; +} + +static int wt_hsm_flash_init(void *context, const void *config) +{ + wt_hsm_flash_context_t *ctx = (wt_hsm_flash_context_t *)context; + const wt_hsm_flash_config_t *cfg = + (const wt_hsm_flash_config_t *)config; + + if (ctx == NULL || cfg == NULL || cfg != wt_hsm_flash_cfg_of(ctx) || + cfg->base == 0u || cfg->size == 0u || + cfg->sector_size == 0u || cfg->program_unit == 0u || + (cfg->size % cfg->sector_size) != 0u || + (cfg->sector_size % cfg->program_unit) != 0u) { + return WH_ERROR_BADARGS; + } + ctx->write_locked = false; + return WH_ERROR_OK; +} + +static int wt_hsm_flash_cleanup(void *context) +{ + if (context == NULL) { + return WH_ERROR_BADARGS; + } + if (wt_arch_thread_unprivileged()) { + return wt_hsm_flash_gate(context, WT_SPM_KS_FLASH_CLEANUP, 0u, 0u, + NULL); + } + return WH_ERROR_OK; +} + +static uint32_t wt_hsm_flash_partition_size(void *context) +{ + const wt_hsm_flash_config_t *cfg = wt_hsm_flash_cfg_of(context); + + if (cfg == NULL) { + return 0u; + } + return cfg->sector_size; +} + +static int wt_hsm_flash_write_lock(void *context, uint32_t offset, + uint32_t size) +{ + wt_hsm_flash_context_t *ctx = (wt_hsm_flash_context_t *)context; + (void)offset; + (void)size; + + if (ctx == NULL) { + return WH_ERROR_BADARGS; + } + ctx->write_locked = true; + return WH_ERROR_OK; +} + +static int wt_hsm_flash_write_unlock(void *context, uint32_t offset, + uint32_t size) +{ + wt_hsm_flash_context_t *ctx = (wt_hsm_flash_context_t *)context; + (void)offset; + (void)size; + + if (ctx == NULL) { + return WH_ERROR_BADARGS; + } + ctx->write_locked = false; + return WH_ERROR_OK; +} + +static int wt_hsm_flash_read(void *context, uint32_t offset, uint32_t size, + uint8_t *data) +{ + const wt_hsm_flash_config_t *cfg = wt_hsm_flash_cfg_of(context); + + if (data == NULL && size != 0u) { + return WH_ERROR_BADARGS; + } + if (wt_arch_thread_unprivileged()) { + return wt_hsm_flash_gate(context, WT_SPM_KS_FLASH_READ, offset, size, + data); + } + if (!wt_flash_range_ok(cfg, offset, size)) { + return WH_ERROR_BADARGS; + } + if (size != 0u) { + (void)memcpy(data, (const uint8_t *)(cfg->base + offset), size); + } + return WH_ERROR_OK; +} + +static int wt_hsm_flash_program(void *context, uint32_t offset, uint32_t size, + const uint8_t *data) +{ + const wt_hsm_flash_config_t *cfg = wt_hsm_flash_cfg_of(context); + wt_hsm_flash_context_t *ctx = (wt_hsm_flash_context_t *)context; + + if (data == NULL && size != 0u) { + return WH_ERROR_BADARGS; + } + if (wt_arch_thread_unprivileged()) { + return wt_hsm_flash_gate(context, WT_SPM_KS_FLASH_PROGRAM, offset, + size, (void *)(uintptr_t)data); + } + if (!wt_flash_range_ok(cfg, offset, size) || + (offset % cfg->program_unit) != 0u || + (size % cfg->program_unit) != 0u) { + return WH_ERROR_BADARGS; + } + if (size != 0u && ctx->write_locked) { + return WH_ERROR_LOCKED; + } + if (size == 0u) { + return WH_ERROR_OK; + } + return wt_hsm_flash_rom_result( + wt_rom_flash_program(wt_hsm_flash_offset(cfg, offset), data, size)); +} + +static int wt_hsm_flash_erase(void *context, uint32_t offset, uint32_t size) +{ + const wt_hsm_flash_config_t *cfg = wt_hsm_flash_cfg_of(context); + wt_hsm_flash_context_t *ctx = (wt_hsm_flash_context_t *)context; + + if (wt_arch_thread_unprivileged()) { + return wt_hsm_flash_gate(context, WT_SPM_KS_FLASH_ERASE, offset, size, + NULL); + } + if (!wt_flash_range_ok(cfg, offset, size) || + (offset % cfg->sector_size) != 0u || + (size % cfg->sector_size) != 0u) { + return WH_ERROR_BADARGS; + } + if (size != 0u && ctx->write_locked) { + return WH_ERROR_LOCKED; + } + if (size == 0u) { + return WH_ERROR_OK; + } + return wt_hsm_flash_rom_result( + wt_rom_flash_erase(wt_hsm_flash_offset(cfg, offset), size)); +} + +static int wt_hsm_flash_verify(void *context, uint32_t offset, uint32_t size, + const uint8_t *data) +{ + const wt_hsm_flash_config_t *cfg = wt_hsm_flash_cfg_of(context); + + if (data == NULL && size != 0u) { + return WH_ERROR_BADARGS; + } + if (wt_arch_thread_unprivileged()) { + return wt_hsm_flash_gate(context, WT_SPM_KS_FLASH_VERIFY, offset, + size, (void *)(uintptr_t)data); + } + if (!wt_flash_range_ok(cfg, offset, size)) { + return WH_ERROR_BADARGS; + } + if (size != 0u && + memcmp((const uint8_t *)(cfg->base + offset), data, size) != 0) { + return WH_ERROR_NOTVERIFIED; + } + return WH_ERROR_OK; +} + +static int wt_hsm_flash_blank_check(void *context, uint32_t offset, + uint32_t size) +{ + const wt_hsm_flash_config_t *cfg = wt_hsm_flash_cfg_of(context); + int rc; + + if (wt_arch_thread_unprivileged()) { + return wt_hsm_flash_gate(context, WT_SPM_KS_FLASH_BLANKCHECK, offset, + size, NULL); + } + if (!wt_flash_range_ok(cfg, offset, size)) { + return WH_ERROR_BADARGS; + } + rc = wt_rom_flash_blank(wt_hsm_flash_offset(cfg, offset), size); + if (rc == WT_ROM_FLASH_ERR_NOTBLANK) { + return WH_ERROR_NOTBLANK; + } + return (rc == WT_ROM_FLASH_OK) ? WH_ERROR_OK : WH_ERROR_BADARGS; +} + +const whFlashCb g_wt_hsm_flash_cb = { + .Init = wt_hsm_flash_init, + .Cleanup = wt_hsm_flash_cleanup, + .PartitionSize = wt_hsm_flash_partition_size, + .WriteLock = wt_hsm_flash_write_lock, + .WriteUnlock = wt_hsm_flash_write_unlock, + .Read = wt_hsm_flash_read, + .Program = wt_hsm_flash_program, + .Erase = wt_hsm_flash_erase, + .Verify = wt_hsm_flash_verify, + .BlankCheck = wt_hsm_flash_blank_check, +}; + +void *wt_hsm_flash_context(void) +{ + return &g_hsm_flash_ctx; +} + +const void *wt_hsm_flash_config(void) +{ + return &g_hsm_flash_cfg; +} + +int wt_hsm_flash_format(void) +{ + return wt_hsm_flash_erase(&g_hsm_flash_ctx, 0u, g_hsm_flash_cfg.size); +} + +/* SERVICE_FWU staging into the wolfBoot update partition: the FWU partition + * erases each target sector lazily, programs the candidate through the ROM + * API, and verifies every block against the Secure alias. Staging must be + * contiguous from offset 0, so a finished candidate has no unwritten gap. + * arm() programs wolfBoot's update trigger in the trailer so the next boot + * swaps; the swapped image is still gated by authenticated launch. */ + +/* Erased high-water mark: every sector below it is blank or holds staged + * data and is never erased again, whatever the partition's sector count. */ +static uint32_t g_fwu_erased_end; +static uint32_t g_fwu_written_end; + +static int wt_fwu_ensure_erased(uint32_t offset, uint32_t size) +{ + uint32_t sector_size = g_fwu_flash_cfg.sector_size; + uint32_t end = offset + size; + uint32_t s; + + end = ((end + sector_size - 1u) / sector_size) * sector_size; + for (s = g_fwu_erased_end; s < end; s += sector_size) { + if (wt_hsm_flash_erase(&g_fwu_flash_ctx, s, sector_size) != + WH_ERROR_OK) { + return -1; + } + g_fwu_erased_end = s + sector_size; + } + return 0; +} + +static int wt_fwu_backend_begin(void *ctx) +{ + (void)ctx; + if (wt_hsm_flash_init(&g_fwu_flash_ctx, &g_fwu_flash_cfg) != WH_ERROR_OK) { + return -1; + } + g_fwu_erased_end = 0u; + g_fwu_written_end = 0u; + return 0; +} + +static int wt_fwu_backend_write(void *ctx, uint32_t offset, + const uint8_t *data, uint32_t size) +{ + const uint8_t *mapped = (const uint8_t *)(g_fwu_flash_cfg.base + offset); + uint32_t reserved = g_fwu_flash_cfg.size - g_fwu_flash_cfg.sector_size; + + (void)ctx; + /* The trailer sector holds wolfBoot's swap trigger and is owned by + * arm/disarm alone; staging data must never pre-program it. */ + if (offset >= reserved || size > reserved - offset || + offset != g_fwu_written_end) { + return -1; + } + if (wt_fwu_ensure_erased(offset, size) != 0) { + return -1; + } + if (wt_hsm_flash_program(&g_fwu_flash_ctx, offset, size, data) != + WH_ERROR_OK) { + return -1; + } + if (memcmp(mapped, data, size) != 0) { + return -1; + } + g_fwu_written_end = offset + size; + return 0; +} + +static int wt_fwu_backend_disarm(void *ctx); + +static int wt_fwu_backend_arm(void *ctx, uint32_t image_size, + uint32_t version) +{ + uint32_t block_off = g_fwu_flash_cfg.size - g_fwu_flash_cfg.program_unit; + const uint8_t *mapped = (const uint8_t *)(g_fwu_flash_cfg.base + block_off); + uint8_t block[WT_FLASH_PHRASE_SIZE]; + + (void)ctx; + (void)image_size; + (void)version; + /* The wolfBoot trigger lives in the topmost program unit of the partition + * (state + magic); the block below it stays erased. The trailer sector + * never holds staged data, so it is erased outright. */ + if (wt_fwu_wolfboot_arm_trailer(block, (uint32_t)sizeof(block)) != 0) { + return -1; + } + if (wt_fwu_backend_disarm(NULL) != 0) { + return -1; + } + if (wt_hsm_flash_program(&g_fwu_flash_ctx, block_off, + (uint32_t)sizeof(block), block) != WH_ERROR_OK || + memcmp(mapped, block, sizeof(block)) != 0) { + /* A partially programmed trigger must not survive a failed arm. */ + (void)wt_fwu_backend_disarm(NULL); + return -1; + } + return 0; +} + +static int wt_fwu_backend_disarm(void *ctx) +{ + uint32_t trailer = g_fwu_flash_cfg.size - g_fwu_flash_cfg.sector_size; + + (void)ctx; + if (wt_hsm_flash_erase(&g_fwu_flash_ctx, trailer, + g_fwu_flash_cfg.sector_size) != WH_ERROR_OK) { + return -1; + } + return 0; +} + +/* wolfBoot image header size for this port (staged images carry it). */ +#define WT_FWU_IMAGE_HEADER_SIZE 0x400u +#define WT_FWU_IMAGE_MAGIC 0x464C4F57u +#define WT_FWU_HDR_TAG_VERSION 0x0001u + +static int wt_fwu_backend_verify(void *ctx, uint32_t staged_size, + uint32_t *header_version) +{ + const uint8_t *hdr = (const uint8_t *)g_fwu_flash_cfg.base; + uint32_t magic; + uint32_t fw_size; + uint32_t version = 0u; + uint32_t offset = 8u; + uint32_t tag; + uint32_t len; + int found = 0; + + (void)ctx; + if (header_version == NULL || + staged_size < WT_FWU_IMAGE_HEADER_SIZE || + staged_size != g_fwu_written_end) { + return -1; + } + memcpy(&magic, hdr, sizeof(magic)); + memcpy(&fw_size, hdr + 4u, sizeof(fw_size)); + if (magic != WT_FWU_IMAGE_MAGIC) { + return -1; + } + /* The staged extent must cover the header plus the declared payload. */ + if (fw_size > staged_size - WT_FWU_IMAGE_HEADER_SIZE) { + return -1; + } + while (offset + 4u <= WT_FWU_IMAGE_HEADER_SIZE) { + if (hdr[offset] == 0xFFu) { + offset++; + continue; + } + tag = (uint32_t)hdr[offset] | ((uint32_t)hdr[offset + 1u] << 8); + len = (uint32_t)hdr[offset + 2u] | ((uint32_t)hdr[offset + 3u] << 8); + if (tag == 0u) { + break; + } + if (offset + 4u + len > WT_FWU_IMAGE_HEADER_SIZE) { + return -1; + } + if (tag == WT_FWU_HDR_TAG_VERSION && len == sizeof(version)) { + memcpy(&version, hdr + offset + 4u, sizeof(version)); + found = 1; + } + offset += 4u + len; + } + if (found == 0) { + return -1; + } + *header_version = version; + return 0; +} + +const wt_fwu_backend_t wt_fwu_flash_backend = { + .begin = wt_fwu_backend_begin, + .write = wt_fwu_backend_write, + .arm = wt_fwu_backend_arm, + .disarm = wt_fwu_backend_disarm, + /* The trailer sector is arm/disarm-owned, never staging capacity. */ + .capacity = WT_FWU_UPDATE_FLASH_SIZE - WT_FLASH_SECTOR_SIZE, + .align = WT_FLASH_PHRASE_SIZE, + .verify = wt_fwu_backend_verify, +}; + +#if defined(WT_REMEASURE_PROBE) +/* Probe-only tamper of guest0's first sector: erase it and program a zero + * phrase where the vector table was, through the same privileged primitives. */ +int wt_hsm_flash_remeasure_tamper(uintptr_t secure_base) +{ + uint8_t block[WT_FLASH_PHRASE_SIZE]; + + if (secure_base != g_remeasure_probe_cfg.base) { + return -1; + } + g_remeasure_probe_ctx.write_locked = false; + if (wt_hsm_flash_erase(&g_remeasure_probe_ctx, 0u, WT_FLASH_SECTOR_SIZE) != + WH_ERROR_OK) { + return -1; + } + (void)memset(block, 0x00, sizeof(block)); + return (wt_hsm_flash_program(&g_remeasure_probe_ctx, 0u, sizeof(block), + block) == WH_ERROR_OK) ? 0 : -1; +} +#endif + +#if defined(WT_CONFORMANCE) && (WT_CONFORMANCE == 1) +static bool g_conf_nvm_ready; + +int wt_conf_nvm_flash_sync(uint8_t *buf, uint32_t len, int store) +{ + int ret; + + if (buf == NULL || len == 0u || len > g_conf_nvm_cfg.size || + (len % g_conf_nvm_cfg.program_unit) != 0u) { + return -1; + } + if (!g_conf_nvm_ready) { + if (wt_hsm_flash_init(&g_conf_nvm_ctx, &g_conf_nvm_cfg) != WH_ERROR_OK) { + return -1; + } + g_conf_nvm_ready = true; + } + if (store == 0) { + ret = wt_hsm_flash_read(&g_conf_nvm_ctx, 0u, len, buf); + } + else { + ret = wt_hsm_flash_erase(&g_conf_nvm_ctx, 0u, + g_conf_nvm_cfg.sector_size); + if (ret == WH_ERROR_OK) { + ret = wt_hsm_flash_program(&g_conf_nvm_ctx, 0u, len, buf); + } + } + return (ret == WH_ERROR_OK) ? 0 : -1; +} +#endif + +#if defined(WT_DEPUTY_NEG_PROBE) && (WT_DEPUTY_NEG_PROBE == 1) +/* A primitive fed a forged offset must reject it with exactly + * WH_ERROR_BADARGS through a delivered gate call. */ +static int wt_deputy_expect_badargs(int32_t sub_op, uint32_t offset, + uint32_t size, void *buf) +{ + wt_spm_call_t call; + + (void)memset(&call, 0, sizeof(call)); + call.op = WT_SPM_OP_KEYSTORE_FLASH; + call.call_type = sub_op; + call.vec_idx = offset; + call.num_bytes = size; + call.buffer = buf; + if (wt_arch_sp_trap(&call) != WT_FFM_SUCCESS) { + return 0; + } + return (call.ret_int == WH_ERROR_BADARGS) ? 1 : 0; +} + +static int wt_deputy_read_ok(int32_t sub_op, uint8_t *buf, uint32_t size) +{ + wt_spm_call_t call; + + (void)memset(&call, 0, sizeof(call)); + call.op = WT_SPM_OP_KEYSTORE_FLASH; + call.call_type = sub_op; + call.vec_idx = 0u; + call.num_bytes = size; + call.buffer = buf; + return (wt_arch_sp_trap(&call) == WT_FFM_SUCCESS && + call.ret_int == WH_ERROR_OK) ? 1 : 0; +} + +/* The vault's writable context holds no geometry here, so a deputy can only + * forge offsets and lengths: each must be refused against the const config, + * and the store must still read back unchanged. */ +__attribute__((used, noinline)) +int wt_platform_deputy_flash_probe(void) +{ + uint8_t ref[WT_FLASH_PHRASE_SIZE]; + uint8_t leak[WT_FLASH_PHRASE_SIZE]; + uint32_t oor; + int ok = 1; + + /* Wraps modulo 2^32 so base + offset addresses SPM-private RAM. */ + oor = (uint32_t)((uintptr_t)WT_RAM_S_BASE - g_hsm_flash_cfg.base); + + (void)memset(ref, 0, sizeof(ref)); + if (wt_deputy_read_ok(WT_SPM_KS_FLASH_READ, ref, sizeof(ref)) == 0) { + return 0; + } + ok &= wt_deputy_expect_badargs(WT_SPM_KS_FLASH_READ, oor, 16u, leak); + ok &= wt_deputy_expect_badargs(WT_SPM_KS_FLASH_PROGRAM, oor, 16u, leak); + ok &= wt_deputy_expect_badargs(WT_SPM_KS_FLASH_ERASE, oor, + g_hsm_flash_cfg.sector_size, NULL); + ok &= wt_deputy_expect_badargs(WT_SPM_KS_FLASH_VERIFY, oor, 16u, leak); + ok &= wt_deputy_expect_badargs(WT_SPM_KS_FLASH_BLANKCHECK, oor, 16u, + NULL); + /* A length running past the store from an in-range offset. */ + ok &= wt_deputy_expect_badargs(WT_SPM_KS_FLASH_READ, + g_hsm_flash_cfg.size - 8u, 16u, leak); + /* Misaligned program and sub-sector erase, checked on the const units. */ + ok &= wt_deputy_expect_badargs(WT_SPM_KS_FLASH_PROGRAM, 4u, 16u, leak); + ok &= wt_deputy_expect_badargs(WT_SPM_KS_FLASH_ERASE, 0u, 16u, NULL); + + (void)memset(leak, 0, sizeof(leak)); + if (wt_deputy_read_ok(WT_SPM_KS_FLASH_READ, leak, sizeof(leak)) == 0 || + memcmp(leak, ref, sizeof(ref)) != 0) { + ok = 0; + } + if (wt_deputy_read_ok(WT_SPM_KS_FLASH_VERIFY, ref, sizeof(ref)) == 0) { + ok = 0; + } + return ok; +} +#endif diff --git a/port/mcxn947/hsm_flash.h b/port/mcxn947/hsm_flash.h new file mode 100644 index 00000000..e772db2e --- /dev/null +++ b/port/mcxn947/hsm_flash.h @@ -0,0 +1,53 @@ +/* hsm_flash.h + * + * 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 . + */ + +#ifndef WOLFTRUST_MCXN947_HSM_FLASH_H +#define WOLFTRUST_MCXN947_HSM_FLASH_H + +#include +#include +#include "wolftrust/port_nvm.h" + +typedef struct wt_hsm_flash_config { + uintptr_t base; + uint32_t size; + uint32_t sector_size; + uint32_t program_unit; +} wt_hsm_flash_config_t; + +typedef struct wt_hsm_flash_context { + bool write_locked; +} wt_hsm_flash_context_t; + +/* The vault's own flash bookkeeping (hsm_flash_ctx.c, in the vault's data + * band). The driver's privileged state stays in hsm_flash.c, in SPM RAM. */ +extern wt_hsm_flash_context_t g_hsm_flash_ctx; +extern volatile uint32_t g_wt_flash_gate_aborts; +extern volatile uint32_t g_wt_flash_gate_abort_info; + +#if defined(WT_CONFORMANCE) && (WT_CONFORMANCE == 1) +int wt_conf_nvm_flash_sync(uint8_t *buf, uint32_t len, int store); +#endif + +#if defined(WT_REMEASURE_PROBE) +int wt_hsm_flash_remeasure_tamper(uintptr_t secure_base); +#endif + +#endif /* WOLFTRUST_MCXN947_HSM_FLASH_H */ diff --git a/port/mcxn947/hsm_flash_ctx.c b/port/mcxn947/hsm_flash_ctx.c new file mode 100644 index 00000000..eda4a766 --- /dev/null +++ b/port/mcxn947/hsm_flash_ctx.c @@ -0,0 +1,27 @@ +/* hsm_flash_ctx.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 "hsm_flash.h" + +wt_hsm_flash_context_t g_hsm_flash_ctx; + +/* Gate-path forensics (SWD-readable), counted in the vault's own thread. */ +volatile uint32_t g_wt_flash_gate_aborts __attribute__((used)); +volatile uint32_t g_wt_flash_gate_abort_info __attribute__((used)); diff --git a/port/mcxn947/l3_port.h b/port/mcxn947/l3_port.h new file mode 100644 index 00000000..c989dfa7 --- /dev/null +++ b/port/mcxn947/l3_port.h @@ -0,0 +1,31 @@ +/* l3_port.h + * + * 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 . + */ + +/* MCXN947 inputs to the shared Armv8-M isolation level 3 layer. */ + +#ifndef WOLFTRUST_FW_MCXN947_L3_PORT_H +#define WOLFTRUST_FW_MCXN947_L3_PORT_H + +#include "mcxn947_regs.h" + +/* A Secure peripheral only the SPM drives, probed by the periphsp negative. */ +#define WT_L3_SPM_PERIPHERAL_BASE WT_ELS_BASE_S + +#endif diff --git a/port/mcxn947/manifest-conformance.json b/port/mcxn947/manifest-conformance.json new file mode 100644 index 00000000..b0cb56af --- /dev/null +++ b/port/mcxn947/manifest-conformance.json @@ -0,0 +1,728 @@ +{ + "format_version": 1, + "generator_version": "WOLFTRUST_GEN_1", + "features": 1, + "isolation_profile": 3, + "profile_capabilities": { + "capabilities": 127, + "max_domains": 11, + "max_memory_resources_per_domain": 3, + "max_interrupts_per_domain": 1 + }, + "domains": [ + { + "id": 0, + "domain_class": 0, + "rot_role": 1, + "security_state": 0, + "privilege_state": 0, + "initial_lifecycle": 1, + "entry_point": 268697600, + "stack_base": 805507072, + "stack_size": 4096, + "memory_resources": [ + { + "base": 268697600, + "size": 4096, + "attributes": 5, + "share_id": 0 + }, + { + "base": 805507072, + "size": 4096, + "attributes": 19, + "share_id": 0 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 2, + "restart_limit": 0, + "restart_window_ticks": 0, + "initial_delay_ticks": 0 + }, + "required_capabilities": 127, + "launch_required": 0, + "launch_min_version": 0 + }, + { + "id": 1, + "domain_class": 2, + "rot_role": 0, + "security_state": 1, + "privilege_state": 0, + "initial_lifecycle": 1, + "entry_point": 524288, + "stack_base": 536870912, + "stack_size": 98304, + "memory_resources": [ + { + "base": 524288, + "size": 524288, + "attributes": 5, + "share_id": 0 + }, + { + "base": 536870912, + "size": 98304, + "attributes": 19, + "share_id": 0 + }, + { + "base": 1074479104, + "size": 4096, + "attributes": 43, + "share_id": 2 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 1, + "restart_limit": 3, + "restart_window_ticks": 8000, + "initial_delay_ticks": 1 + }, + "required_capabilities": 80, + "launch_required": 1, + "launch_min_version": 1 + }, + { + "id": 2, + "domain_class": 2, + "rot_role": 0, + "security_state": 1, + "privilege_state": 0, + "initial_lifecycle": 1, + "entry_point": 1048576, + "stack_base": 536969216, + "stack_size": 98304, + "memory_resources": [ + { + "base": 1048576, + "size": 262144, + "attributes": 5, + "share_id": 0 + }, + { + "base": 536969216, + "size": 98304, + "attributes": 19, + "share_id": 0 + }, + { + "base": 1074479104, + "size": 4096, + "attributes": 43, + "share_id": 2 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 1, + "restart_limit": 3, + "restart_window_ticks": 8000, + "initial_delay_ticks": 1 + }, + "required_capabilities": 80, + "launch_required": 1, + "launch_min_version": 1 + }, + { + "id": 3, + "domain_class": 1, + "rot_role": 2, + "security_state": 0, + "privilege_state": 1, + "initial_lifecycle": 0, + "entry_point": 268763136, + "stack_base": 805699584, + "stack_size": 8192, + "memory_resources": [ + { + "base": 268763136, + "size": 4096, + "attributes": 5, + "share_id": 0 + }, + { + "base": 805699584, + "size": 8192, + "attributes": 19, + "share_id": 0 + }, + { + "base": 805603328, + "size": 2048, + "attributes": 19, + "share_id": 0 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 1, + "restart_limit": 3, + "restart_window_ticks": 8000, + "initial_delay_ticks": 1 + }, + "required_capabilities": 80, + "launch_required": 0, + "launch_min_version": 0 + }, + { + "id": 4, + "domain_class": 1, + "rot_role": 2, + "security_state": 0, + "privilege_state": 1, + "initial_lifecycle": 0, + "entry_point": 268767232, + "stack_base": 805691392, + "stack_size": 8192, + "memory_resources": [ + { + "base": 268767232, + "size": 4096, + "attributes": 5, + "share_id": 0 + }, + { + "base": 805691392, + "size": 8192, + "attributes": 19, + "share_id": 0 + }, + { + "base": 805605376, + "size": 32768, + "attributes": 19, + "share_id": 0 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 1, + "restart_limit": 3, + "restart_window_ticks": 8000, + "initial_delay_ticks": 1 + }, + "required_capabilities": 80, + "launch_required": 0, + "launch_min_version": 0 + }, + { + "id": 5, + "domain_class": 1, + "rot_role": 2, + "security_state": 0, + "privilege_state": 1, + "initial_lifecycle": 0, + "entry_point": 268783616, + "stack_base": 805662720, + "stack_size": 16384, + "memory_resources": [ + { + "base": 268783616, + "size": 4096, + "attributes": 5, + "share_id": 0 + }, + { + "base": 805662720, + "size": 16384, + "attributes": 19, + "share_id": 0 + }, + { + "base": 805595136, + "size": 8192, + "attributes": 19, + "share_id": 0 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 1, + "restart_limit": 3, + "restart_window_ticks": 8000, + "initial_delay_ticks": 1 + }, + "required_capabilities": 80, + "launch_required": 0, + "launch_min_version": 0 + }, + { + "id": 6, + "domain_class": 1, + "rot_role": 2, + "security_state": 0, + "privilege_state": 1, + "initial_lifecycle": 0, + "entry_point": 268787712, + "stack_base": 805654528, + "stack_size": 8192, + "memory_resources": [ + { + "base": 268787712, + "size": 4096, + "attributes": 5, + "share_id": 0 + }, + { + "base": 805654528, + "size": 8192, + "attributes": 19, + "share_id": 0 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 1, + "restart_limit": 3, + "restart_window_ticks": 8000, + "initial_delay_ticks": 1 + }, + "required_capabilities": 80, + "launch_required": 0, + "launch_min_version": 0 + }, + { + "id": 7, + "domain_class": 1, + "rot_role": 2, + "security_state": 0, + "privilege_state": 1, + "initial_lifecycle": 0, + "entry_point": 268791808, + "stack_base": 805646336, + "stack_size": 8192, + "memory_resources": [ + { + "base": 268791808, + "size": 4096, + "attributes": 5, + "share_id": 0 + }, + { + "base": 805646336, + "size": 8192, + "attributes": 19, + "share_id": 0 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 1, + "restart_limit": 3, + "restart_window_ticks": 8000, + "initial_delay_ticks": 1 + }, + "required_capabilities": 80, + "launch_required": 0, + "launch_min_version": 0 + }, + { + "id": 8, + "domain_class": 1, + "rot_role": 3, + "security_state": 0, + "privilege_state": 1, + "initial_lifecycle": 0, + "entry_point": 268771328, + "stack_base": 805707776, + "stack_size": 8192, + "memory_resources": [ + { + "base": 268771328, + "size": 4096, + "attributes": 5, + "share_id": 0 + }, + { + "base": 805707776, + "size": 8192, + "attributes": 19, + "share_id": 0 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 1, + "restart_limit": 3, + "restart_window_ticks": 8000, + "initial_delay_ticks": 1 + }, + "required_capabilities": 80, + "launch_required": 0, + "launch_min_version": 0 + }, + { + "id": 9, + "domain_class": 1, + "rot_role": 2, + "security_state": 0, + "privilege_state": 1, + "initial_lifecycle": 0, + "entry_point": 268775424, + "stack_base": 805715968, + "stack_size": 8192, + "memory_resources": [ + { + "base": 268775424, + "size": 4096, + "attributes": 5, + "share_id": 0 + }, + { + "base": 805715968, + "size": 8192, + "attributes": 19, + "share_id": 0 + } + ], + "interrupt_resources": [ + { + "interrupt": 36, + "attributes": 0, + "share_id": 0 + } + ], + "restart_policy": { + "action": 1, + "restart_limit": 3, + "restart_window_ticks": 8000, + "initial_delay_ticks": 1 + }, + "required_capabilities": 80, + "launch_required": 0, + "launch_min_version": 0 + }, + { + "id": 10, + "domain_class": 1, + "rot_role": 3, + "security_state": 0, + "privilege_state": 1, + "initial_lifecycle": 0, + "entry_point": 268779520, + "stack_base": 805724160, + "stack_size": 8192, + "memory_resources": [ + { + "base": 268779520, + "size": 4096, + "attributes": 5, + "share_id": 0 + }, + { + "base": 805724160, + "size": 8192, + "attributes": 19, + "share_id": 0 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 1, + "restart_limit": 3, + "restart_window_ticks": 8000, + "initial_delay_ticks": 1 + }, + "required_capabilities": 80, + "launch_required": 0, + "launch_min_version": 0 + } + ], + "partitions": [ + { + "name": "PARTITION_ATTEST", + "domain_id": 3, + "framework_version": 256, + "model": 0, + "priority": 2, + "services": [ + { + "name": "SERVICE_ATTEST", + "sid": 4096, + "version": 1, + "version_policy": 0, + "signal": 16, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + } + ], + "dependencies": [ + 4102 + ], + "interrupts": [] + }, + { + "name": "PARTITION_HSM", + "domain_id": 4, + "framework_version": 256, + "model": 0, + "priority": 1, + "services": [ + { + "name": "SERVICE_HSM", + "sid": 4102, + "version": 1, + "version_policy": 0, + "signal": 32, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + } + ], + "dependencies": [ + 4098 + ], + "interrupts": [] + }, + { + "name": "PARTITION_VAULT", + "domain_id": 5, + "framework_version": 256, + "model": 0, + "priority": 1, + "services": [ + { + "name": "SERVICE_VAULT", + "sid": 4098, + "version": 1, + "version_policy": 0, + "signal": 16, + "stateless_handle_index": 0, + "nonsecure_clients": false, + "connection_based": true + } + ], + "dependencies": [], + "interrupts": [] + }, + { + "name": "PARTITION_ITS", + "domain_id": 6, + "framework_version": 256, + "model": 0, + "priority": 1, + "services": [ + { + "name": "SERVICE_ITS", + "sid": 4099, + "version": 1, + "version_policy": 0, + "signal": 16, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + } + ], + "dependencies": [ + 4098 + ], + "interrupts": [] + }, + { + "name": "PARTITION_PS", + "domain_id": 7, + "framework_version": 256, + "model": 0, + "priority": 1, + "services": [ + { + "name": "SERVICE_PS", + "sid": 4100, + "version": 1, + "version_policy": 0, + "signal": 16, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + } + ], + "dependencies": [ + 4098 + ], + "interrupts": [] + }, + { + "name": "SERVER_PARTITION", + "domain_id": 8, + "framework_version": 256, + "model": 0, + "priority": 1, + "services": [ + { + "name": "SERVER_TEST_DISPATCHER", + "sid": 64257, + "version": 1, + "version_policy": 1, + "signal": 16, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + }, + { + "name": "SERVER_SECURE_CONNECT_ONLY", + "sid": 64258, + "version": 2, + "version_policy": 1, + "signal": 32, + "stateless_handle_index": 0, + "nonsecure_clients": false, + "connection_based": true + }, + { + "name": "SERVER_STRICT_VERSION", + "sid": 64259, + "version": 2, + "version_policy": 0, + "signal": 64, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + }, + { + "name": "SERVER_UNSPECIFIED_VERSION", + "sid": 64260, + "version": 1, + "version_policy": 0, + "signal": 128, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + }, + { + "name": "SERVER_RELAX_VERSION", + "sid": 64261, + "version": 2, + "version_policy": 1, + "signal": 256, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + }, + { + "name": "SERVER_UNEXTERN", + "sid": 64262, + "version": 2, + "version_policy": 1, + "signal": 512, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + }, + { + "name": "SERVER_CONNECTION_DROP", + "sid": 64263, + "version": 2, + "version_policy": 1, + "signal": 1024, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + } + ], + "dependencies": [ + 64513, + 64515 + ], + "interrupts": [] + }, + { + "name": "DRIVER_PARTITION", + "domain_id": 9, + "framework_version": 256, + "model": 0, + "priority": 1, + "services": [ + { + "name": "DRIVER_UART", + "sid": 64513, + "version": 1, + "version_policy": 1, + "signal": 16, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + }, + { + "name": "DRIVER_WATCHDOG", + "sid": 64514, + "version": 1, + "version_policy": 1, + "signal": 32, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + }, + { + "name": "DRIVER_NVMEM", + "sid": 64515, + "version": 1, + "version_policy": 1, + "signal": 64, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + }, + { + "name": "DRIVER_TEST", + "sid": 64516, + "version": 1, + "version_policy": 1, + "signal": 128, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + } + ], + "dependencies": [], + "interrupts": [ + { + "signal_name": "DRIVER_UART_INTR_SIG", + "interrupt": 36, + "signal": 256 + } + ] + }, + { + "name": "CLIENT_PARTITION", + "domain_id": 10, + "framework_version": 256, + "model": 0, + "priority": 1, + "services": [ + { + "name": "CLIENT_TEST_DISPATCHER", + "sid": 64001, + "version": 1, + "version_policy": 1, + "signal": 16, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + } + ], + "dependencies": [ + 64513, + 64515, + 64516, + 64257, + 64260, + 64259, + 64261, + 64258, + 64263 + ], + "interrupts": [] + } + ], + "limits": { + "max_partitions": 8, + "max_services_per_partition": 7, + "max_dependencies_per_partition": 9, + "max_stateless_handles": 32 + } +} diff --git a/port/mcxn947/manifest.json b/port/mcxn947/manifest.json new file mode 100644 index 00000000..532f56b7 --- /dev/null +++ b/port/mcxn947/manifest.json @@ -0,0 +1,501 @@ +{ + "format_version": 1, + "generator_version": "WOLFTRUST_GEN_1", + "features": 1, + "isolation_profile": 3, + "profile_capabilities": { + "capabilities": 127, + "max_domains": 9, + "max_memory_resources_per_domain": 3, + "max_interrupts_per_domain": 0 + }, + "domains": [ + { + "id": 0, + "domain_class": 0, + "rot_role": 1, + "security_state": 0, + "privilege_state": 0, + "initial_lifecycle": 1, + "entry_point": 268697600, + "stack_base": 805507072, + "stack_size": 4096, + "memory_resources": [ + { + "base": 268697600, + "size": 4096, + "attributes": 5, + "share_id": 0 + }, + { + "base": 805507072, + "size": 4096, + "attributes": 19, + "share_id": 0 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 2, + "restart_limit": 0, + "restart_window_ticks": 0, + "initial_delay_ticks": 0 + }, + "required_capabilities": 127, + "launch_required": 0, + "launch_min_version": 0 + }, + { + "id": 1, + "domain_class": 2, + "rot_role": 0, + "security_state": 1, + "privilege_state": 0, + "initial_lifecycle": 1, + "entry_point": 524288, + "stack_base": 536870912, + "stack_size": 98304, + "memory_resources": [ + { + "base": 524288, + "size": 524288, + "attributes": 5, + "share_id": 0 + }, + { + "base": 536870912, + "size": 98304, + "attributes": 19, + "share_id": 0 + }, + { + "base": 1074479104, + "size": 4096, + "attributes": 43, + "share_id": 2 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 1, + "restart_limit": 3, + "restart_window_ticks": 8000, + "initial_delay_ticks": 1 + }, + "required_capabilities": 80, + "launch_required": 1, + "launch_min_version": 1 + }, + { + "id": 2, + "domain_class": 2, + "rot_role": 0, + "security_state": 1, + "privilege_state": 0, + "initial_lifecycle": 1, + "entry_point": 1048576, + "stack_base": 536969216, + "stack_size": 98304, + "memory_resources": [ + { + "base": 1048576, + "size": 262144, + "attributes": 5, + "share_id": 0 + }, + { + "base": 536969216, + "size": 98304, + "attributes": 19, + "share_id": 0 + }, + { + "base": 1074479104, + "size": 4096, + "attributes": 43, + "share_id": 2 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 1, + "restart_limit": 3, + "restart_window_ticks": 8000, + "initial_delay_ticks": 1 + }, + "required_capabilities": 80, + "launch_required": 1, + "launch_min_version": 1 + }, + { + "id": 3, + "domain_class": 1, + "rot_role": 2, + "security_state": 0, + "privilege_state": 1, + "initial_lifecycle": 0, + "entry_point": 268763136, + "stack_base": 805699584, + "stack_size": 8192, + "memory_resources": [ + { + "base": 268763136, + "size": 4096, + "attributes": 5, + "share_id": 0 + }, + { + "base": 805699584, + "size": 8192, + "attributes": 19, + "share_id": 0 + }, + { + "base": 805603328, + "size": 2048, + "attributes": 19, + "share_id": 0 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 1, + "restart_limit": 3, + "restart_window_ticks": 8000, + "initial_delay_ticks": 1 + }, + "required_capabilities": 80, + "launch_required": 0, + "launch_min_version": 0 + }, + { + "id": 4, + "domain_class": 1, + "rot_role": 2, + "security_state": 0, + "privilege_state": 1, + "initial_lifecycle": 0, + "entry_point": 268767232, + "stack_base": 805691392, + "stack_size": 8192, + "memory_resources": [ + { + "base": 268767232, + "size": 4096, + "attributes": 5, + "share_id": 0 + }, + { + "base": 805691392, + "size": 8192, + "attributes": 19, + "share_id": 0 + }, + { + "base": 805605376, + "size": 32768, + "attributes": 19, + "share_id": 0 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 1, + "restart_limit": 3, + "restart_window_ticks": 8000, + "initial_delay_ticks": 1 + }, + "required_capabilities": 80, + "launch_required": 0, + "launch_min_version": 0 + }, + { + "id": 5, + "domain_class": 1, + "rot_role": 2, + "security_state": 0, + "privilege_state": 1, + "initial_lifecycle": 0, + "entry_point": 268783616, + "stack_base": 805662720, + "stack_size": 16384, + "memory_resources": [ + { + "base": 268783616, + "size": 4096, + "attributes": 5, + "share_id": 0 + }, + { + "base": 805662720, + "size": 16384, + "attributes": 19, + "share_id": 0 + }, + { + "base": 805595136, + "size": 8192, + "attributes": 19, + "share_id": 0 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 1, + "restart_limit": 3, + "restart_window_ticks": 8000, + "initial_delay_ticks": 1 + }, + "required_capabilities": 80, + "launch_required": 0, + "launch_min_version": 0 + }, + { + "id": 6, + "domain_class": 1, + "rot_role": 2, + "security_state": 0, + "privilege_state": 1, + "initial_lifecycle": 0, + "entry_point": 268787712, + "stack_base": 805654528, + "stack_size": 8192, + "memory_resources": [ + { + "base": 268787712, + "size": 4096, + "attributes": 5, + "share_id": 0 + }, + { + "base": 805654528, + "size": 8192, + "attributes": 19, + "share_id": 0 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 1, + "restart_limit": 3, + "restart_window_ticks": 8000, + "initial_delay_ticks": 1 + }, + "required_capabilities": 80, + "launch_required": 0, + "launch_min_version": 0 + }, + { + "id": 7, + "domain_class": 1, + "rot_role": 2, + "security_state": 0, + "privilege_state": 1, + "initial_lifecycle": 0, + "entry_point": 268791808, + "stack_base": 805646336, + "stack_size": 8192, + "memory_resources": [ + { + "base": 268791808, + "size": 4096, + "attributes": 5, + "share_id": 0 + }, + { + "base": 805646336, + "size": 8192, + "attributes": 19, + "share_id": 0 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 1, + "restart_limit": 3, + "restart_window_ticks": 8000, + "initial_delay_ticks": 1 + }, + "required_capabilities": 80, + "launch_required": 0, + "launch_min_version": 0 + }, + { + "id": 8, + "domain_class": 1, + "rot_role": 2, + "security_state": 0, + "privilege_state": 1, + "initial_lifecycle": 0, + "entry_point": 268795904, + "stack_base": 805638144, + "stack_size": 8192, + "memory_resources": [ + { + "base": 268795904, + "size": 4096, + "attributes": 5, + "share_id": 0 + }, + { + "base": 805638144, + "size": 8192, + "attributes": 19, + "share_id": 0 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 1, + "restart_limit": 3, + "restart_window_ticks": 8000, + "initial_delay_ticks": 1 + }, + "required_capabilities": 80, + "launch_required": 0, + "launch_min_version": 0 + } + ], + "partitions": [ + { + "name": "PARTITION_ATTEST", + "domain_id": 3, + "framework_version": 256, + "model": 0, + "priority": 2, + "services": [ + { + "name": "SERVICE_ATTEST", + "sid": 4096, + "version": 1, + "version_policy": 0, + "signal": 16, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + } + ], + "dependencies": [ + 4102 + ], + "interrupts": [] + }, + { + "name": "PARTITION_HSM", + "domain_id": 4, + "framework_version": 256, + "model": 0, + "priority": 1, + "services": [ + { + "name": "SERVICE_HSM", + "sid": 4102, + "version": 1, + "version_policy": 0, + "signal": 32, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + } + ], + "dependencies": [ + 4098 + ], + "interrupts": [] + }, + { + "name": "PARTITION_VAULT", + "domain_id": 5, + "framework_version": 256, + "model": 0, + "priority": 1, + "services": [ + { + "name": "SERVICE_VAULT", + "sid": 4098, + "version": 1, + "version_policy": 0, + "signal": 16, + "stateless_handle_index": 0, + "nonsecure_clients": false, + "connection_based": true + } + ], + "dependencies": [], + "interrupts": [] + }, + { + "name": "PARTITION_ITS", + "domain_id": 6, + "framework_version": 256, + "model": 0, + "priority": 1, + "services": [ + { + "name": "SERVICE_ITS", + "sid": 4099, + "version": 1, + "version_policy": 0, + "signal": 16, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + } + ], + "dependencies": [ + 4098 + ], + "interrupts": [] + }, + { + "name": "PARTITION_PS", + "domain_id": 7, + "framework_version": 256, + "model": 0, + "priority": 1, + "services": [ + { + "name": "SERVICE_PS", + "sid": 4100, + "version": 1, + "version_policy": 0, + "signal": 16, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + } + ], + "dependencies": [ + 4098 + ], + "interrupts": [] + }, + { + "name": "PARTITION_FWU", + "domain_id": 8, + "framework_version": 256, + "model": 0, + "priority": 1, + "services": [ + { + "name": "SERVICE_FWU", + "sid": 4101, + "version": 1, + "version_policy": 0, + "signal": 16, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + } + ], + "dependencies": [], + "interrupts": [] + } + ], + "limits": { + "max_partitions": 6, + "max_services_per_partition": 1, + "max_dependencies_per_partition": 1, + "max_stateless_handles": 32 + } +} diff --git a/port/mcxn947/mcxn947_regs.h b/port/mcxn947/mcxn947_regs.h new file mode 100644 index 00000000..c3676759 --- /dev/null +++ b/port/mcxn947/mcxn947_regs.h @@ -0,0 +1,70 @@ +/* mcxn947_regs.h + * + * 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 . + */ + +#ifndef WOLFTRUST_MCXN947_REGS_H +#define WOLFTRUST_MCXN947_REGS_H + +#include + +/* Secure-alias peripheral bases (MCXN947 cm33_core0, NS alias = base - + * 0x10000000). */ +#define WT_PERIPH_NS_BASE 0x40000000u +#define WT_PERIPH_S_BASE 0x50000000u +#define WT_PERIPH_ALIAS_SIZE 0x10000000u + +#define WT_SYSCON_BASE_S 0x50000000u +#define WT_FMU0_BASE_S 0x50043000u +#define WT_ELS_BASE_S 0x50054000u +#define WT_ELS_BASE_NS 0x40054000u +#define WT_LPUART4_BASE_S 0x500B4000u +#define WT_LPUART4_BASE_NS 0x400B4000u +#define WT_TRDC_BASE_S 0x500C7000u +#define WT_AHBSC_BASE_S 0x50120000u + +/* Boot ROM through its Secure alias; the ROM flash API tree sits at its end. */ +#define WT_ROM_BASE_S 0x13000000u +#define WT_ROM_SIZE 0x00040000u +#define WT_ROM_API_TREE 0x1303FC00u + +#define WT_REG32(address) (*(volatile uint32_t*)(address)) + +/* EdgeLock (ELS, S50) command interface; register offsets per the MCUXpresso + * S50 peripheral header. */ +#define WT_ELS_STATUS WT_REG32(WT_ELS_BASE_S + 0x00u) +#define WT_ELS_CTRL WT_REG32(WT_ELS_BASE_S + 0x04u) +#define WT_ELS_CMDCFG0 WT_REG32(WT_ELS_BASE_S + 0x08u) +#define WT_ELS_DMA_RES0 WT_REG32(WT_ELS_BASE_S + 0x38u) +#define WT_ELS_DMA_RES0_LEN WT_REG32(WT_ELS_BASE_S + 0x3Cu) +#define WT_ELS_ERR_STATUS WT_REG32(WT_ELS_BASE_S + 0x4Cu) +#define WT_ELS_ERR_STATUS_CLR WT_REG32(WT_ELS_BASE_S + 0x50u) +#define WT_ELS_STATUS_BUSY (1u << 0) +#define WT_ELS_STATUS_IRQ (1u << 1) +#define WT_ELS_STATUS_ERR (1u << 2) +#define WT_ELS_STATUS_LOCKED (1u << 16) +#define WT_ELS_CTRL_EN (1u << 0) +#define WT_ELS_CTRL_START (1u << 1) +#define WT_ELS_CTRL_RESET (1u << 2) +#define WT_ELS_CTRL_CMD(cmd) (((uint32_t)(cmd) & 0x1Fu) << 3) +#define WT_ELS_CMD_RND_REQ 24u +#define WT_ELS_ERR_STATUS_CLR_ALL (1u << 0) +/* RND_REQ fills the DMA result buffer in whole words. */ +#define WT_ELS_RND_ALIGN 4u + +#endif /* WOLFTRUST_MCXN947_REGS_H */ diff --git a/port/mcxn947/memory_map.h b/port/mcxn947/memory_map.h new file mode 100644 index 00000000..09509fe2 --- /dev/null +++ b/port/mcxn947/memory_map.h @@ -0,0 +1,108 @@ +/* memory_map.h + * + * 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 . + */ + +#ifndef WOLFTRUST_FW_MCXN947_MEMORY_MAP_H +#define WOLFTRUST_FW_MCXN947_MEMORY_MAP_H + +/* MCXN947 CPU0. Bit 28 of every address selects the Secure alias: the 2 MiB + * internal flash is 0x00000000 (NS) / 0x10000000 (S), RAMA-H is 0x20000000 + * (NS) / 0x30000000 (S), peripherals 0x40000000 (NS) / 0x50000000 (S). */ +#ifndef WT_SECURE_FLASH_BASE +#define WT_SECURE_FLASH_BASE 0x10040000u +#endif +#ifndef WT_SECURE_FLASH_SIZE +#define WT_SECURE_FLASH_SIZE 0x00040000u +#endif +#ifndef WT_SECURE_IMAGE_HEADER_SIZE +#define WT_SECURE_IMAGE_HEADER_SIZE 0x400u +#endif +#define WT_FLASH_S_BASE WT_SECURE_FLASH_BASE +#define WT_FLASH_S_SIZE WT_SECURE_FLASH_SIZE +#define WT_FLASH_IMAGE_BASE (WT_FLASH_S_BASE + WT_SECURE_IMAGE_HEADER_SIZE) +/* The SG veneers sit in the image's second 1 KiB, as on the STM32H5: the + * flash is in the Code region, where the IDAU honours an SAU NSC window. */ +#define WT_NSC_BASE (WT_FLASH_IMAGE_BASE + 0x00000400u) +#define WT_NSC_END (WT_NSC_BASE + 0x000003FFu) + +#define WT_FLASH_NS_BASE 0x00000000u +#define WT_FLASH_S_ALIAS_BASE 0x10000000u +#define WT_FLASH_SIZE 0x00200000u +#define WT_FLASH_TO_S_ALIAS(address) \ + ((address) - WT_FLASH_NS_BASE + WT_FLASH_S_ALIAS_BASE) +#ifndef WT_GUEST0_FLASH_BASE +#define WT_GUEST0_FLASH_BASE 0x00080000u +#endif +#ifndef WT_GUEST1_FLASH_BASE +#define WT_GUEST1_FLASH_BASE 0x00100000u +#endif +#ifndef WT_GUEST0_FLASH_SIZE +#define WT_GUEST0_FLASH_SIZE 0x00080000u +#endif +#ifndef WT_GUEST1_FLASH_SIZE +#define WT_GUEST1_FLASH_SIZE 0x00040000u +#endif + +/* Flash geometry: 8 KiB erase sectors, 16-byte ECC phrases that are written + * once per erase. Every store below the Secure image lives in bank 1, so the + * image keeps executing from bank 0 while the ROM API programs them. */ +#define WT_FLASH_SECTOR_SIZE 0x00002000u +#define WT_FLASH_PHRASE_SIZE 0x00000010u +#define WT_FLASH_BANK_SIZE 0x00100000u +#define WT_HSM_NVM_FLASH_BASE_NS 0x001E0000u +#define WT_HSM_NVM_FLASH_BASE_S 0x101E0000u +#define WT_HSM_NVM_FLASH_SIZE 0x00004000u + +#define WT_CONF_NVM_FLASH_BASE_NS 0x001E4000u +#define WT_CONF_NVM_FLASH_BASE_S 0x101E4000u +#define WT_CONF_NVM_FLASH_SIZE 0x00002000u + +/* Non-secure guest RAM: two 96 KiB windows at the bottom of RAMA-H. Their + * Secure alias holds the first loader's RAM, dead after the jump, which + * wt_platform_init zeroes before any guest runs. */ +#define WT_RAM_NS_BASE 0x20000000u +#define WT_PLATFORM_GUEST_STACK_WINDOW_BASE WT_RAM_NS_BASE +#define WT_PLATFORM_GUEST_STACK_WINDOW_SIZE 0x00030000u +#define WT_GUEST0_RAM_BASE 0x20000000u +#define WT_GUEST1_RAM_BASE 0x20018000u +#define WT_GUEST_RAM_SIZE 0x00018000u + +/* Secure runtime RAM: the top 220 KiB of RAMA-H through its Secure alias, + * 4 KiB above the wolfBoot handoff record (a 56-byte record; the rest of + * that page is scratch the core clears once the record is consumed). The + * isolation level 3 bands are offsets from this base. */ +#define WT_RAM_S_BASE 0x30031000u +#define WT_RAM_S_SIZE 0x00037000u +#define WT_BOOT_HANDOFF_ADDRESS 0x30030000u +#define WT_L3_SPM_RAM_SIZE 0x00015800u +#define WT_SP_HSM_DATA_SIZE 0x00008000u + +#include "../common/armv8m/l3_layout.h" + +/* wolfBoot update partition (Secure alias, bank 1). */ +#define WT_FWU_UPDATE_FLASH_BASE_S 0x10180000u +#define WT_FWU_UPDATE_FLASH_SIZE 0x00040000u + +#define WT_SHARED_STATUS_ADDR 0x20000000u + +#ifndef WT_PLATFORM_CORE_CLOCK_HZ +#define WT_PLATFORM_CORE_CLOCK_HZ 150000000u +#endif + +#endif diff --git a/port/mcxn947/partitions.c b/port/mcxn947/partitions.c new file mode 100644 index 00000000..abb0e3ea --- /dev/null +++ b/port/mcxn947/partitions.c @@ -0,0 +1,504 @@ +/* partitions.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/context.h" +#include "wolftrust/guest_verify.h" +#include "wolftrust/partition.h" +#include "memory_map.h" +#include "mcxn947_regs.h" + +#include + +/* Pinned guest-measurement slot. The image-assembly patcher locates it by + * magic inside wolftrust.bin, stamps the guest digests, and only then is the + * image signed for wolfBoot, so the pins share the image's root of trust. + * An unpatched slot advertises zero records, which fails launch closed. */ +#define WT_GUEST_MEAS_SLOT_MAGIC_LEN 16u +#define WT_GUEST_MEAS_SLOT_UNPATCHED 0xFFFFFFFFu + +/* Enforcement the MCXN947 port provides (WT-PORT-0008). The fabric filter is + * the per-dispatch SAU window, as on the MIMXRT700: it isolates CPU0's guests + * from each other and says nothing about CPU1, DMA or any other bus master, + * which the TRDC and AHB secure controller are not yet programmed to fence. */ +#define WT_MCXN947_PORT_CAPABILITIES \ + (WT_PORT_CAPABILITY_VECTOR_READ_ALIAS | \ + WT_PORT_CAPABILITY_NS_DOMAIN_PROGRAMMING | \ + WT_PORT_CAPABILITY_TZ_FILTER) +#if defined(__ARM_EABI__) +#define WT_GUEST_MEAS_SECTION \ + __attribute__((section(".wt_guest_meas"), used, aligned(4))) +#else +#define WT_GUEST_MEAS_SECTION +#endif + +typedef struct wt_guest_meas_slot { + uint8_t magic[WT_GUEST_MEAS_SLOT_MAGIC_LEN]; + uint32_t count; + wt_guest_measurement_t records[WT_GUEST_MEAS_MAX_RECORDS]; +} wt_guest_meas_slot_t; + +static const wt_guest_meas_slot_t g_guest_meas_slot WT_GUEST_MEAS_SECTION = { + { 0x57u, 0x54u, 0x47u, 0x4Du, 0x45u, 0x41u, 0x53u, 0x31u, + 0xA5u, 0x3Cu, 0x96u, 0xE1u, 0x78u, 0x0Fu, 0xB2u, 0x4Bu }, + WT_GUEST_MEAS_SLOT_UNPATCHED, + { { 0u, 0u, 0u, { 0u } } } +}; + +static wt_guest_measurement_t g_guest_meas_copy[WT_GUEST_MEAS_MAX_RECORDS]; + +const wt_guest_measurement_t* wt_platform_guest_measurements(size_t* count) +{ + /* The slot is stamped into the binary after linking, so this const + * object's initializer lies: every field is loaded through volatile, or + * the optimizer folds the unpatched zeros into the records it returns. */ + const volatile uint32_t* slot_count = &g_guest_meas_slot.count; + const volatile uint8_t* src = + (const volatile uint8_t*)g_guest_meas_slot.records; + uint8_t* dst = (uint8_t*)g_guest_meas_copy; + uint32_t records = *slot_count; + size_t bytes; + size_t i; + + if (count == NULL) { + return NULL; + } + + if (records == WT_GUEST_MEAS_SLOT_UNPATCHED || + records > WT_GUEST_MEAS_MAX_RECORDS) { + *count = 0u; + return NULL; + } + + bytes = (size_t)records * sizeof(wt_guest_measurement_t); + for (i = 0u; i < bytes; i++) { + dst[i] = src[i]; + } + *count = (size_t)records; + return g_guest_meas_copy; +} + +#ifndef WT_TIMESLICE_MS +#define WT_TIMESLICE_MS 2U +#endif + +#ifndef WT_SHARED_UART +#define WT_SHARED_UART 0 +#endif + +/* Both guests share LPUART4 (LP_FLEXCOMM4), the FRDM board's debug console. */ +#define WT_USART_REGION_SIZE 0x00001000U +#define WT_GUEST0_USART_BASE WT_LPUART4_BASE_NS +#define WT_GUEST1_USART_BASE WT_LPUART4_BASE_NS +/* Initial restore runs from a Secure exception and returns to a Non-secure + * Thread/MSP frame; ES stays set because the exception was taken to Secure + * state (clearing it trips INVPC on Armv8-M silicon). */ +#define WT_EXC_RETURN_NS_THREAD_MSP_FROM_SECURE 0xFFFFFFB9U + +static wt_guest_config_t g_partition_configs[] = { + { + .guest_id = 0U, + .name = "guest-a", + .vector_table = WT_GUEST0_FLASH_BASE, + .initial_psp_ns = 0x00000000U, + .initial_msp_ns = WT_GUEST0_RAM_BASE + WT_GUEST_RAM_SIZE, + .irq_mask = { + .words = {0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U} + }, + .memory_windows = { + {WT_GUEST0_FLASH_BASE, WT_GUEST0_FLASH_SIZE, + WT_MEM_ATTR_READ | WT_MEM_ATTR_EXEC, WT_RESOURCE_SHARE_NONE}, + {WT_GUEST0_RAM_BASE, WT_GUEST_RAM_SIZE, + WT_MEM_ATTR_READ | WT_MEM_ATTR_WRITE | WT_MEM_ATTR_RESTART_CLEAR, + WT_RESOURCE_SHARE_NONE} + }, + .memory_window_count = 2U, + .memory_regions = { + {WT_GUEST0_FLASH_BASE, WT_GUEST0_FLASH_SIZE, + WT_MEM_ATTR_READ | WT_MEM_ATTR_EXEC}, + {WT_GUEST0_RAM_BASE, WT_GUEST_RAM_SIZE, + WT_MEM_ATTR_READ | WT_MEM_ATTR_WRITE}, + {WT_GUEST0_USART_BASE, WT_USART_REGION_SIZE, + WT_MEM_ATTR_READ | WT_MEM_ATTR_WRITE | WT_MEM_ATTR_DEVICE}, + /* NSC window: NS guests must be able to fetch the SG veneers. + * Per ARMv8-M, NSC fetches succeed when SAU marks them NSC AND + * the NS MPU grants execute permission. */ + {WT_NSC_BASE, (WT_NSC_END - WT_NSC_BASE + 1U), + WT_MEM_ATTR_READ | WT_MEM_ATTR_EXEC} + }, + .memory_region_count = 4U, + .restart_policy = { + .restart_limit = 3U, + .restart_window_ticks = 64U, + .initial_delay_ticks = 1U + }, + .initial_state = WT_GUEST_READY, + .timeslice_ms = WT_TIMESLICE_MS, + .port = { + .required_capabilities = WT_MCXN947_PORT_CAPABILITIES, + .provided_capabilities = WT_MCXN947_PORT_CAPABILITIES, + .vector_read_address = + WT_FLASH_TO_S_ALIAS(WT_GUEST0_FLASH_BASE) + } + }, +#if WT_MAX_GUESTS > 1 + { + .guest_id = 1U, + .name = "guest-b", + .vector_table = WT_GUEST1_FLASH_BASE, + .initial_psp_ns = 0x00000000U, + .initial_msp_ns = WT_GUEST1_RAM_BASE + WT_GUEST_RAM_SIZE, + .irq_mask = { + .words = {0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U} + }, + .memory_windows = { + {WT_GUEST1_FLASH_BASE, WT_GUEST1_FLASH_SIZE, + WT_MEM_ATTR_READ | WT_MEM_ATTR_EXEC, WT_RESOURCE_SHARE_NONE}, + {WT_GUEST1_RAM_BASE, WT_GUEST_RAM_SIZE, + WT_MEM_ATTR_READ | WT_MEM_ATTR_WRITE | WT_MEM_ATTR_RESTART_CLEAR, + WT_RESOURCE_SHARE_NONE} + }, + .memory_window_count = 2U, + .memory_regions = { + {WT_GUEST1_FLASH_BASE, WT_GUEST1_FLASH_SIZE, + WT_MEM_ATTR_READ | WT_MEM_ATTR_EXEC}, + {WT_GUEST1_RAM_BASE, WT_GUEST_RAM_SIZE, + WT_MEM_ATTR_READ | WT_MEM_ATTR_WRITE}, + {WT_GUEST1_USART_BASE, WT_USART_REGION_SIZE, + WT_MEM_ATTR_READ | WT_MEM_ATTR_WRITE | WT_MEM_ATTR_DEVICE}, + /* NSC window: see guest-a above. */ + {WT_NSC_BASE, (WT_NSC_END - WT_NSC_BASE + 1U), + WT_MEM_ATTR_READ | WT_MEM_ATTR_EXEC} + }, + .memory_region_count = 4U, + .restart_policy = { + .restart_limit = 3U, + .restart_window_ticks = 64U, + .initial_delay_ticks = 1U + }, + .initial_state = WT_GUEST_READY, + .timeslice_ms = WT_TIMESLICE_MS, + .port = { + .required_capabilities = WT_MCXN947_PORT_CAPABILITIES, + .provided_capabilities = WT_MCXN947_PORT_CAPABILITIES, + .vector_read_address = + WT_FLASH_TO_S_ALIAS(WT_GUEST1_FLASH_BASE) + } + } +#endif +}; + +static wt_guest_runtime_t g_partition_runtime[ + sizeof(g_partition_configs) / sizeof(g_partition_configs[0]) +]; +/* Port-owned concrete context storage; the neutral runtime holds pointers. */ +static wt_guest_context_t g_partition_contexts[ + sizeof(g_partition_configs) / sizeof(g_partition_configs[0]) +]; + +static void wt_partitions_wire_contexts(void) +{ + size_t i; + + for (i = 0; i < sizeof(g_partition_runtime) / + sizeof(g_partition_runtime[0]); ++i) { + g_partition_runtime[i].context = &g_partition_contexts[i]; + } +} +static const wt_profile_capabilities_t g_profile_capabilities = { + .capabilities = WT_CAPABILITY_SECURITY_STATE | + WT_CAPABILITY_PRIVILEGE_STATE | + WT_CAPABILITY_ROT_ISOLATION | + WT_CAPABILITY_DOMAIN_ISOLATION | + WT_CAPABILITY_MEMORY_PROTECTION | + WT_CAPABILITY_INTERRUPT_ISOLATION | + WT_CAPABILITY_RESTART, + .max_domains = 11U, + .max_memory_resources_per_domain = 3U, + .max_interrupts_per_domain = 1U, +}; +static uintptr_t g_bound_exec_bases[ + sizeof(g_partition_configs) / sizeof(g_partition_configs[0]) +]; +static size_t g_bound_exec_sizes[ + sizeof(g_partition_configs) / sizeof(g_partition_configs[0]) +]; + +static bool wt_guest_reset_handler_valid(const wt_guest_config_t* config, + uintptr_t resetHandler) +{ + uintptr_t entry = resetHandler & ~(uintptr_t)1u; + + return ((resetHandler & 1u) != 0u) && + (entry >= config->vector_table) && + (entry < (config->vector_table + config->memory_windows[0].size)); +} + +static uintptr_t wt_guest_reset_handler(const wt_guest_config_t* config) +{ + uintptr_t resetHandler; + + resetHandler = ((const uint32_t*)config->port.vector_read_address)[1]; + if (!wt_guest_reset_handler_valid(config, resetHandler)) { + /* Read the vector through the Secure alias first; fall back to the + * Non-secure alias only after rejecting the value as an invalid guest + * entry (the IDAU may return zero through the Secure alias). */ + resetHandler = ((const uint32_t*)config->vector_table)[1]; + } + + if (!wt_guest_reset_handler_valid(config, resetHandler)) { + return 0u; + } + + return resetHandler; +} + +const wt_guest_config_t* wt_partitions_config_table(size_t* count) +{ + if (count != NULL) { + *count = sizeof(g_partition_configs) / sizeof(g_partition_configs[0]); + } + + return g_partition_configs; +} + +wt_guest_runtime_t* wt_partitions_runtime_table(size_t* count) +{ + wt_partitions_wire_contexts(); + if (count != NULL) { + *count = sizeof(g_partition_runtime) / sizeof(g_partition_runtime[0]); + } + + return g_partition_runtime; +} + +const wt_profile_capabilities_t* wt_partitions_profile_capabilities(void) +{ + return &g_profile_capabilities; +} + +static const wt_domain_descriptor_t* wt_partition_manifest_domain( + const wt_system_manifest_t* manifest, wt_domain_id_t id) +{ + size_t i; + + for (i = 0U; i < manifest->domain_count; ++i) { + if (manifest->domains[i].id == id) { + return &manifest->domains[i]; + } + } + + return NULL; +} + +int wt_partitions_bind_manifest(const wt_system_manifest_t* manifest) +{ + size_t count; + size_t i; + + if (manifest == NULL || manifest->domains == NULL) { + return -1; + } + + count = sizeof(g_partition_configs) / sizeof(g_partition_configs[0]); + for (i = 0U; i < count; ++i) { + wt_guest_config_t* config = &g_partition_configs[i]; + const wt_domain_descriptor_t* domain; + + /* Domain zero is the SPM; each guest is a Non-secure application + * domain one higher in the generated manifest. The Secure Partitions + * (crypto, attestation) are separate secure domains above the guests + * and are not bound to a guest here. */ + if (config->guest_id == WT_DOMAIN_ID_INVALID) { + return -1; + } + domain = wt_partition_manifest_domain(manifest, + (wt_domain_id_t)config->guest_id + 1U); + /* Guests launch privileged (CONTROL_NS = 0), so declare PRIVILEGED. */ + if (domain == NULL || + domain->domain_class != WT_DOMAIN_CLASS_NONSECURE_APPLICATION || + domain->security_state != WT_SECURITY_STATE_NONSECURE || + domain->privilege_state != WT_PRIVILEGE_STATE_PRIVILEGED || + domain->restart_policy.action != WT_RESTART_ACTION_DOMAIN) { + return -1; + } + if (wt_partition_validate_port_binding(config, domain) != + WT_PORT_VALID) { + return -1; + } + + /* The generated manifest's restart policy is authoritative: the SPM + * honors the declared limits, not a compiled-in copy. */ + config->restart_policy.restart_limit = + domain->restart_policy.restart_limit; + config->restart_policy.restart_window_ticks = + domain->restart_policy.restart_window_ticks; + config->restart_policy.initial_delay_ticks = + domain->restart_policy.initial_delay_ticks; + /* The manifest's declared initial lifecycle drives the runtime state: + * an NS application declared READY boots runnable; STOPPED stays out + * of the schedule until an explicit lifecycle action. */ + config->initial_state = (wt_guest_state_t)domain->initial_lifecycle; + config->launch_required = domain->launch_required; + config->launch_min_version = domain->launch_min_version; + + /* Rebuild the entire NS MPU table from declared policy: memory + * resources become caller-band windows, the declared console UART is + * the only device grant accepted (pinned to the compiled address), + * and unused slots are cleared so no static grant survives. */ + if (domain->memory_resource_count > WT_MAX_MEMORY_REGIONS - 1U) { + return -1; + } + size_t window_count = 0U; + size_t region_count = 0U; + for (size_t resource = 0U; + resource < domain->memory_resource_count; ++resource) { + const wt_memory_resource_t* manifest_resource = + &domain->memory_resources[resource]; + uint32_t mpu_attributes = manifest_resource->attributes & + (WT_MEM_ATTR_READ | WT_MEM_ATTR_WRITE | WT_MEM_ATTR_EXEC | + WT_MEM_ATTR_DEVICE); + + if ((manifest_resource->attributes & WT_MEM_ATTR_DEVICE) != 0U) { + uintptr_t usart_base = (config->guest_id == 0U) ? + WT_GUEST0_USART_BASE : WT_GUEST1_USART_BASE; + + if (manifest_resource->base != usart_base || + manifest_resource->size != WT_USART_REGION_SIZE) { + return -1; + } + } + else { + if (window_count >= config->memory_window_count) { + return -1; + } + config->memory_windows[window_count] = *manifest_resource; + window_count++; + } + config->memory_regions[region_count].base = manifest_resource->base; + config->memory_regions[region_count].size = manifest_resource->size; + config->memory_regions[region_count].attributes = mpu_attributes; + region_count++; + } + if (window_count != config->memory_window_count) { + return -1; + } + /* NSC veneer fetch window: a platform policy object every NS domain + * needs to reach the SG gateway (SAU NSC + NS MPU execute). */ + config->memory_regions[region_count].base = WT_NSC_BASE; + config->memory_regions[region_count].size = + WT_NSC_END - WT_NSC_BASE + 1U; + config->memory_regions[region_count].attributes = + WT_MEM_ATTR_READ | WT_MEM_ATTR_EXEC; + region_count++; + for (size_t clear = region_count; clear < WT_MAX_MEMORY_REGIONS; + ++clear) { + config->memory_regions[clear].base = 0U; + config->memory_regions[clear].size = 0U; + config->memory_regions[clear].attributes = 0U; + } + config->memory_region_count = region_count; + + if (domain->entry_point == 0U || + domain->interrupt_resource_count > WT_MAX_IRQ_WORDS * 32U || + (domain->interrupt_resource_count != 0U && + domain->interrupt_resources == NULL)) { + return -1; + } + memset(&config->irq_mask, 0, sizeof(config->irq_mask)); + for (size_t interrupt = 0U; + interrupt < domain->interrupt_resource_count; ++interrupt) { + uint32_t irq = domain->interrupt_resources[interrupt].interrupt; + size_t word = irq / 32U; + uint32_t bit = irq % 32U; + + if (word >= WT_MAX_IRQ_WORDS) { + return -1; + } + config->irq_mask.words[word] |= (uint32_t)1U << bit; + } + g_bound_exec_bases[i] = 0U; + g_bound_exec_sizes[i] = 0U; + for (size_t executable = 0U; + executable < domain->memory_resource_count; ++executable) { + const wt_memory_resource_t* resource = + &domain->memory_resources[executable]; + + if ((resource->attributes & WT_MEM_ATTR_EXEC) != 0U) { + g_bound_exec_bases[i] = resource->base; + g_bound_exec_sizes[i] = resource->size; + break; + } + } + if (g_bound_exec_sizes[i] == 0U) { + return -1; + } + + if (domain->stack_base > (uintptr_t)-1 - domain->stack_size) { + return -1; + } + config->initial_msp_ns = domain->stack_base + domain->stack_size; + } + + return 0; +} + +void wt_partition_reset_runtime(const wt_guest_config_t* config, + wt_guest_runtime_t* runtime) +{ + uint32_t restart_count; + uint32_t first_restart_tick; + + if (config == NULL || runtime == NULL) { + return; + } + + restart_count = runtime->restart_count; + first_restart_tick = runtime->first_restart_tick; + memset(runtime, 0, sizeof(*runtime)); + /* The memset wipes the port-wired context pointer; rewire by table + * index and zero the concrete context storage instead. */ + runtime->context = + &g_partition_contexts[(size_t)(runtime - g_partition_runtime)]; + memset(runtime->context, 0, sizeof(*runtime->context)); + runtime->restart_count = restart_count; + runtime->first_restart_tick = first_restart_tick; + runtime->state = config->initial_state; + runtime->context->psp_ns = config->initial_psp_ns; + runtime->context->msp_ns = config->initial_msp_ns; + runtime->context->vector_table_ns = config->vector_table; + /* Reset PC is the guest's reset-handler pointer at vector[1]; the slot + * already carries the Thumb bit. wt_jump_to_ns strips it before BXNS. + * Read via the Secure alias of the flash: a Secure-side read of the NS + * alias may return zero, so vector_read_address is the 0x1... alias. */ + runtime->context->pc = wt_guest_reset_handler(config); + if (config->guest_id >= + sizeof(g_bound_exec_bases) / sizeof(g_bound_exec_bases[0]) || + g_bound_exec_sizes[config->guest_id] == 0U || + runtime->context->pc < g_bound_exec_bases[config->guest_id] || + runtime->context->pc >= g_bound_exec_bases[config->guest_id] + + g_bound_exec_sizes[config->guest_id]) { + runtime->context->pc = 0U; + return; + } + runtime->context->lr = 0U; + runtime->context->xpsr = 0x01000000U; + runtime->context->exc_return = WT_EXC_RETURN_NS_THREAD_MSP_FROM_SECURE; + runtime->context->frame_stacked = false; +} diff --git a/port/mcxn947/platform_mcxn947.c b/port/mcxn947/platform_mcxn947.c new file mode 100644 index 00000000..9b429e65 --- /dev/null +++ b/port/mcxn947/platform_mcxn947.c @@ -0,0 +1,363 @@ +/* platform_mcxn947.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/platform.h" +#include "wolftrust/arch.h" +#include "wolftrust/guest_verify.h" +#include "wolftrust/monitor.h" +#include "wolftrust/arch/armv8m/context.h" +#include "wolftrust/arch/armv8m/armv8m.h" +#include "wolftrust/arch/armv8m/core_regs.h" + +#include +#include +#include +#include + +#include "memory_map.h" +#include "mcxn947_regs.h" + +#include "wolftrust/fabric_windows.h" + +#include "wolftrust/ffm_gateway.h" +#include "wolftrust/spm_transport.h" +#include "wolftrust/ffm.h" +#include "wolftrust/ffm_boot.h" +#include "wolftrust/ffm_domain.h" +#include "psa_manifest/pid.h" +#include "wolftrust/services/hsm.h" +#include "wolftrust/boot_handoff.h" +#include "wolftrust/services/initial_attestation.h" +#include "wolftrust/arch/armv8m/cmse.h" +#include "wolftrust/sched/tasklet.h" + +static volatile uint32_t g_secure_service_depth; +static volatile uint32_t g_hsm_wait_skip_count; + +/* The IDAU already marks every bit-28-clear alias Non-secure; the SAU must + * agree or the more secure attribute wins. The guest RAM extent is NOT a + * static Non-secure region: each dispatch marks only the arriving guest's + * window Non-secure through the dynamic regions below, so the peer's window + * stays Secure by SAU default while it runs, as on the MIMXRT700. */ +static const wt_armv8m_sau_region_t g_sau_regions[] = { + { WT_GUEST0_FLASH_BASE, + WT_GUEST1_FLASH_BASE + WT_GUEST1_FLASH_SIZE - 1u, false }, + { WT_NSC_BASE, WT_NSC_END, true }, + /* Only the console: the manifest accepts no other Non-secure device grant, + * so a guest with its MPU off still faults on every other peripheral. */ + { WT_LPUART4_BASE_NS, WT_LPUART4_BASE_NS + 0x00000FFFu, false }, +}; + +/* Dynamic SAU regions for the per-dispatch guest RAM windows, above the static + * table (three entries above). CM33 implements eight SAU regions; a guest + * declares one writable RAM window, so this headroom covers every manifest. */ +#define WT_SAU_DYN_FIRST 3u +#define WT_SAU_DYN_LAST 6u + +static uint32_t g_sau_dyn_next; + +/* Secure-side MPU whitelist, programmed with PRIVDEFENA off by the arch + * layer and replayed after every Secure Partition domain. */ +static const wt_armv8m_mpu_region_t g_mpu_s_whitelist[] = { + /* Region 0: secure image RX (vectors, NSC stubs, .text, .rodata). */ + { WT_FLASH_S_BASE, WT_FLASH_S_BASE + WT_FLASH_S_SIZE - 1u, + WT_MPU_RBAR_AP_RO | WT_MPU_RBAR_SH_INNER, + WT_MPU_RLAR_ATTRIDX_NORMAL }, + + /* Region 1: the Secure alias of everything above the image (guest + * images, update and swap partitions, the wolfHSM and conformance NVM + * stores): read-only XN, written only through the ROM flash API. */ + { WT_FLASH_TO_S_ALIAS(WT_GUEST0_FLASH_BASE), + WT_FLASH_S_ALIAS_BASE + WT_FLASH_SIZE - 1u, + WT_MPU_RBAR_XN | WT_MPU_RBAR_AP_RO | WT_MPU_RBAR_SH_INNER, + WT_MPU_RLAR_ATTRIDX_NOCACHE }, + + /* Region 2: secure RAM RW-NX from the boot handoff record up. */ + { WT_BOOT_HANDOFF_ADDRESS, + WT_RAM_S_BASE + WT_RAM_S_SIZE - 1u, + WT_MPU_RBAR_XN | WT_MPU_RBAR_AP_RW | WT_MPU_RBAR_SH_INNER, + WT_MPU_RLAR_ATTRIDX_NORMAL }, + + /* Region 3: NS guest RAM RW-NX through the 0x20000000 alias (HSM + * transport buffers and fault-response CSRs). */ + { WT_RAM_NS_BASE, + WT_RAM_NS_BASE + WT_PLATFORM_GUEST_STACK_WINDOW_SIZE - 1u, + WT_MPU_RBAR_XN | WT_MPU_RBAR_AP_RW | WT_MPU_RBAR_SH_INNER, + WT_MPU_RLAR_ATTRIDX_NORMAL }, + + /* Region 4: flash NS alias, read-only XN for guest image metadata. */ + { WT_FLASH_NS_BASE, WT_FLASH_NS_BASE + WT_FLASH_SIZE - 1u, + WT_MPU_RBAR_XN | WT_MPU_RBAR_AP_RO | WT_MPU_RBAR_SH_INNER, + WT_MPU_RLAR_ATTRIDX_NORMAL }, + + /* Region 5: peripheral aperture, both aliases (SYSCON, FMU, ELS, + * LPUART, TRDC, AHBSC). */ + { WT_PERIPH_NS_BASE, WT_PERIPH_S_BASE + WT_PERIPH_ALIAS_SIZE - 1u, + WT_MPU_RBAR_XN | WT_MPU_RBAR_AP_RW, + WT_MPU_RLAR_ATTRIDX_DEVICE }, + + /* Region 6: Cortex-M33 private peripheral bus. */ + { 0xE0000000u, 0xE00FFFFFu, + WT_MPU_RBAR_XN | WT_MPU_RBAR_AP_RW, + WT_MPU_RLAR_ATTRIDX_DEVICE }, + + /* Region 7: the boot ROM through its Secure alias, privileged + * read-execute: the flash API the SPM programs the stores with. */ + { WT_ROM_BASE_S, WT_ROM_BASE_S + WT_ROM_SIZE - 1u, + WT_MPU_RBAR_AP_RO | WT_MPU_RBAR_SH_INNER, + WT_MPU_RLAR_ATTRIDX_NORMAL }, +}; + +/* No hardware guest-flash fence is read back yet, so a guest window is never + * reported as protected; a WT_GUEST_FLASH_WRP=1 build refuses every guest. */ +int wt_platform_guest_flash_wrp_ok(uintptr_t window_base, size_t window_size) +{ + (void)window_base; + (void)window_size; + return WT_GUEST_VERIFY_ERROR_WRP; +} + +void wt_platform_init(void) +{ + wt_ffm_gateway_install(); + WT_SCB_VTOR_S = WT_FLASH_IMAGE_BASE; + wt_armv8m_sau_init(g_sau_regions, + sizeof(g_sau_regions) / sizeof(g_sau_regions[0])); + wt_armv8m_mpu_s_init(g_mpu_s_whitelist, + sizeof(g_mpu_s_whitelist) / + sizeof(g_mpu_s_whitelist[0])); + wt_arch_init(); + /* The guest windows alias the first loader's RAM, dead after the jump. */ + wt_arch_zero_guest_memory(WT_GUEST0_RAM_BASE, WT_GUEST_RAM_SIZE); + wt_arch_zero_guest_memory(WT_GUEST1_RAM_BASE, WT_GUEST_RAM_SIZE); + g_secure_service_depth = 0u; + g_hsm_wait_skip_count = 0u; +} + +#if defined(WT_LAUNCH_DEBUG) +/* Per-guest boot launch-verification result, read over the debug port. */ +volatile int32_t g_wt_launch_debug[2] __attribute__((used)); + +void wt_platform_launch_debug(int code, uint32_t guest) +{ + if (guest < (sizeof(g_wt_launch_debug) / sizeof(g_wt_launch_debug[0]))) { + g_wt_launch_debug[guest] = (int32_t)code; + } +} +#endif + +/* SAU guest-isolation back-end. close_all disables the dynamic regions so the + * whole guest RAM extent falls to the SAU default (Secure); open_window marks + * one guest window Non-secure in the next free dynamic region. A Non-secure + * store into the peer's now-Secure window raises a SecureFault the monitor + * contains. Mirrors the STM32H5 GTZC path through the shared window helper. */ +static void wt_sau_close_all(void) +{ + uint32_t rnr; + + for (rnr = WT_SAU_DYN_FIRST; rnr <= WT_SAU_DYN_LAST; ++rnr) { + wt_armv8m_sau_program_region(rnr, 0u, 0u, false, false); + } + g_sau_dyn_next = WT_SAU_DYN_FIRST; +} + +static void wt_sau_open_window(uintptr_t base, size_t size) +{ + if (g_sau_dyn_next <= WT_SAU_DYN_LAST) { + wt_armv8m_sau_program_region(g_sau_dyn_next, (uint32_t)base, + (uint32_t)(base + size - 1u), false, true); + ++g_sau_dyn_next; + } +} + +static void wt_sau_commit(void) +{ + wt_dsb(); + wt_isb(); +} + +void wt_platform_program_memory_windows(const wt_memory_window_t* windows, + size_t count) +{ + static const wt_fabric_windows_t fabric = { + WT_RAM_NS_BASE, + WT_RAM_NS_BASE + WT_PLATFORM_GUEST_STACK_WINDOW_SIZE, + wt_sau_close_all, + wt_sau_open_window, + wt_sau_commit + }; + + wt_fabric_apply_windows(&fabric, windows, count); +} + +#if defined(WT_CONFORMANCE) && (WT_CONFORMANCE == 1) +/* PAL interrupt source: the unprivileged DRIVER partition asks for its line + * to fire, so the privileged side pends it in the NVIC. */ +void wt_conf_uart_irq_set(int on) +{ + if (on != 0) { +#if WT_CONF_IRQ >= 32u + WT_NVIC_ISPR1 = (1u << (WT_CONF_IRQ - 32u)); +#else + WT_NVIC_ISPR0 = (1u << WT_CONF_IRQ); +#endif + } + else { + wt_arch_secure_irq_disable(WT_CONF_IRQ); + } +} + +void WT_CONF_IRQ_HANDLER(void) +{ + wt_spm_conf_irq(WT_CONF_IRQ); +} + +#endif + +void wt_platform_log_fault(wt_guest_id_t guest_id, + wt_fault_reason_t reason, + uintptr_t fault_address, + uintptr_t pc) +{ + (void)guest_id; + (void)reason; + wt_armv8m_note_fault(fault_address, pc); +} + +void wt_platform_all_guests_faulted(void) +{ + __asm volatile("bkpt #0x7D"); + for (;;) { + __asm volatile("wfi"); + } +} + +void wt_platform_panic(void) +{ + __asm volatile("bkpt #0x7E"); + for (;;) { + } +} + +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(); + for (;;) { + } +} + +#if defined(WT_REMEASURE_PROBE) +extern int wt_hsm_flash_remeasure_tamper(uintptr_t secure_base); + +void wt_platform_remeasure_probe(void) +{ + const wt_guest_config_t *configs; + size_t cfg_count; + const wt_memory_window_t *window = NULL; + uint32_t mpu_ctrl; + size_t i; + int r1; + int r2; + + configs = wt_partitions_config_table(&cfg_count); + if (configs != NULL && cfg_count > 0u) { + for (i = 0u; i < configs[0].memory_window_count; i++) { + if ((configs[0].memory_windows[i].attributes & + WT_MEM_ATTR_EXEC) != 0u) { + window = &configs[0].memory_windows[i]; + break; + } + } + } + if (window != NULL) { + int tamper; + + r1 = wt_runtime_verify_guest(0u); + mpu_ctrl = WT_MPU_S_CTRL; + WT_MPU_S_CTRL = 0u; + wt_dsb(); + wt_isb(); + tamper = wt_hsm_flash_remeasure_tamper(WT_FLASH_TO_S_ALIAS(window->base)); + WT_MPU_S_CTRL = mpu_ctrl; + wt_dsb(); + wt_isb(); + r2 = wt_runtime_verify_guest(0u); + /* The verdict needs the tamper to have landed: a driver error must + * not pass as a detected tamper. */ + if (r1 == 0 && tamper == 0 && r2 != 0) { + __asm volatile("bkpt #0x6C"); + } + } + for (;;) { + __asm volatile("wfi"); + } +} +#endif + +#if defined(WT_BOOTUPDATE_PROBE) +#include "wolftrust/services/fwu_service.h" +extern const wt_fwu_backend_t wt_fwu_flash_backend; + +void wt_platform_bootupdate_probe(uint32_t running_version) +{ + uint32_t mpu_ctrl; + int armed = -1; + + if (running_version != 1u) { + return; + } + if (wt_fwu_flash_backend.begin != NULL && + wt_fwu_flash_backend.begin(NULL) == 0 && + wt_fwu_flash_backend.arm != NULL) { + mpu_ctrl = WT_MPU_S_CTRL; + WT_MPU_S_CTRL = 0u; + wt_dsb(); + wt_isb(); + armed = wt_fwu_flash_backend.arm(NULL, 0u, 2u); + WT_MPU_S_CTRL = mpu_ctrl; + wt_dsb(); + wt_isb(); + } + if (armed == 0) { + wt_platform_system_reset(); + } +} +#endif + +#ifdef WT_ENGINE_HSM + +bool wt_platform_secure_service_active(void) +{ + return g_secure_service_depth != 0u; +} + +void wt_platform_note_hsm_wait_skip(wt_guest_id_t guest_id) +{ + (void)guest_id; + g_hsm_wait_skip_count++; +} + +#endif /* WT_ENGINE_HSM */ diff --git a/port/mcxn947/rng_entropy.c b/port/mcxn947/rng_entropy.c new file mode 100644 index 00000000..75a0dd94 --- /dev/null +++ b/port/mcxn947/rng_entropy.c @@ -0,0 +1,144 @@ +/* rng_entropy.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 + +#include "mcxn947_regs.h" + +#include "wolftrust/spm_transport.h" +#include "wolftrust/spm_gate.h" +#include "wolftrust/arch.h" + +int wolftrust_rng_generate_block(unsigned char *output, unsigned int sz); +int wolftrust_rng_generate_block_direct(unsigned char *output, + unsigned int sz); + +#define WT_ELS_SPINS 0x00200000u +#define WT_ELS_PAGE_WORDS 16u + +static bool s_rng_ready; + +static int wt_els_wait_idle(void) +{ + uint32_t spins = 0u; + uint32_t status = WT_ELS_STATUS; + + while ((status & WT_ELS_STATUS_BUSY) != 0u && spins < WT_ELS_SPINS) { + ++spins; + status = WT_ELS_STATUS; + } + if ((status & (WT_ELS_STATUS_BUSY | WT_ELS_STATUS_ERR | + WT_ELS_STATUS_LOCKED)) != 0u) { + return -1; + } + return 0; +} + +static int wt_els_init(void) +{ + WT_ELS_CTRL = WT_ELS_CTRL_EN; + return wt_els_wait_idle(); +} + +/* One RND_REQ fills a whole page the ELS writes by DMA; words a request does + * not use are wiped, never served to a later request. */ +static int wt_els_rnd_page(uint32_t *page) +{ + int ret; + + ret = wt_els_wait_idle(); + if (ret == 0) { + WT_ELS_DMA_RES0 = (uint32_t)(uintptr_t)page; + WT_ELS_DMA_RES0_LEN = WT_ELS_PAGE_WORDS * (uint32_t)sizeof(uint32_t); + WT_ELS_CMDCFG0 = 0u; + WT_ELS_CTRL = WT_ELS_CTRL_EN | WT_ELS_CTRL_START | + WT_ELS_CTRL_CMD(WT_ELS_CMD_RND_REQ); + ret = wt_els_wait_idle(); + if (ret != 0) { + WT_ELS_ERR_STATUS_CLR = WT_ELS_ERR_STATUS_CLR_ALL; + } + } + return ret; +} + +static int wt_els_fill(unsigned char *output, unsigned int sz) +{ + uint32_t page[WT_ELS_PAGE_WORDS] __attribute__((aligned(WT_ELS_RND_ALIGN))); + volatile uint32_t* wipe = page; + unsigned int done = 0u; + unsigned int chunk; + unsigned int i; + int ret = 0; + + while (ret == 0 && done < sz) { + ret = wt_els_rnd_page(page); + if (ret == 0) { + chunk = sz - done; + if (chunk > (unsigned int)sizeof(page)) { + chunk = (unsigned int)sizeof(page); + } + (void)memcpy(output + done, page, chunk); + done += chunk; + } + } + for (i = 0u; i < WT_ELS_PAGE_WORDS; ++i) { + wipe[i] = 0u; + } + return ret; +} + +int wolftrust_rng_generate_block_direct(unsigned char *output, unsigned int sz) +{ + if (output == NULL && sz != 0u) { + return -1; + } + if (!s_rng_ready) { + if (wt_els_init() != 0) { + return -1; + } + s_rng_ready = true; + } + if (sz == 0u) { + return 0; + } + return wt_els_fill(output, sz); +} + +int wolftrust_rng_generate_block(unsigned char *output, unsigned int sz) +{ + /* A confined keystore partition cannot touch the ELS; the SVC + * dispatcher runs the direct half privileged. */ + if (wt_arch_thread_unprivileged()) { + wt_spm_call_t call; + + (void)memset(&call, 0, sizeof(call)); + call.op = WT_SPM_OP_KEYSTORE_ENTROPY; + call.buffer = output; + call.num_bytes = sz; + if (wt_spm_sp_call(&call) != WT_FFM_SUCCESS) { + return -1; + } + return call.ret_int; + } + return wolftrust_rng_generate_block_direct(output, sz); +} diff --git a/port/mcxn947/rom_flash.c b/port/mcxn947/rom_flash.c new file mode 100644 index 00000000..df5befb2 --- /dev/null +++ b/port/mcxn947/rom_flash.c @@ -0,0 +1,251 @@ +/* rom_flash.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 . + */ + +/* MCXN947 internal flash program and erase through the boot ROM's flash API, + * called directly from its API tree so the port needs no vendor SDK. */ + +#include +#include + +#include "memory_map.h" +#include "mcxn947_regs.h" +#include "rom_flash.h" + +/* Layouts derived from MCUXpresso fsl_flash.c (bootloader_tree_t, the flash + * driver interface prefix) and fsl_flash.h (flash_config_t, 72 bytes). */ +typedef struct wt_rom_flash_api { + uint32_t version; + int32_t (*init)(void *config); + int32_t (*erase)(void *config, uint32_t start, uint32_t length, + uint32_t key); + int32_t (*program)(void *config, uint32_t start, uint8_t *src, + uint32_t length); + int32_t (*verify_erase)(void *config, uint32_t start, uint32_t length); + int32_t (*verify_program)(void *config, uint32_t start, uint32_t length, + const uint8_t *expected, uint32_t *failed_address, + uint32_t *failed_data); + int32_t (*get_property)(void *config, uint32_t property, uint32_t *value); +} wt_rom_flash_api_t; + +typedef struct wt_rom_api_tree { + void (*run_bootloader)(void *arg); + uint32_t version; + const char *copyright; + uint32_t reserved0; + const wt_rom_flash_api_t *flash; +} wt_rom_api_tree_t; + +#define WT_ROM_FLASH_CONFIG_WORDS 18u +#define WT_ROM_FLASH_STATUS_OK 0 +#define WT_ROM_FLASH_ERASE_KEY 0x6B65666Cu +#define WT_ROM_FLASH_PROP_SECTOR 0x00u +#define WT_ROM_FLASH_PROP_TOTAL 0x01u + +#ifndef WT_ROM_API_TREE_ADDR +#define WT_ROM_API_TREE_ADDR WT_ROM_API_TREE +#endif +#ifndef WT_ROM_FLASH_VIEW_BASE +#define WT_ROM_FLASH_VIEW_BASE WT_FLASH_S_ALIAS_BASE +#endif + +/* SYSCON LPCAC_CTRL: the code-bus cache in front of the flash is cleared + * after every change so a verify reads the array, not a stale line. */ +#define WT_SYSCON_LPCAC_CTRL WT_REG32(WT_SYSCON_BASE_S + 0x824u) +#define WT_SYSCON_LPCAC_CTRL_CLR (1u << 1) + +static uint32_t g_rom_flash_config[WT_ROM_FLASH_CONFIG_WORDS]; +static int g_rom_flash_ready; +/* Last ROM status, read over the debug port by the harness. */ +volatile uint32_t g_wt_rom_flash_last_status __attribute__((used)); + +static const wt_rom_flash_api_t *wt_rom_flash_api(void) +{ + const wt_rom_api_tree_t *tree = + (const wt_rom_api_tree_t *)(uintptr_t)WT_ROM_API_TREE_ADDR; + + return tree->flash; +} + +static const volatile uint8_t *wt_rom_flash_view(uint32_t offset) +{ + return (const volatile uint8_t *)((uintptr_t)WT_ROM_FLASH_VIEW_BASE + + offset); +} + +static uint32_t wt_rom_flash_irq_mask(void) +{ + uint32_t primask = 0u; + +#if defined(__ARM_EABI__) + __asm volatile("mrs %0, primask\n\tcpsid i" : "=r"(primask) :: "memory"); +#endif + return primask; +} + +static void wt_rom_flash_irq_restore(uint32_t primask) +{ +#if defined(__ARM_EABI__) + __asm volatile("msr primask, %0" :: "r"(primask) : "memory"); +#else + (void)primask; +#endif +} + +static void wt_rom_flash_cache_clear(void) +{ +#if defined(__ARM_EABI__) + WT_SYSCON_LPCAC_CTRL |= WT_SYSCON_LPCAC_CTRL_CLR; + __asm volatile("dsb sy\n\tisb" ::: "memory"); +#endif +} + +static int wt_rom_flash_range_ok(uint32_t offset, uint32_t size, + uint32_t unit) +{ + if (size == 0u || (offset % unit) != 0u || (size % unit) != 0u) { + return 0; + } + if (offset > WT_FLASH_SIZE || size > WT_FLASH_SIZE - offset) { + return 0; + } + return 1; +} + +static int wt_rom_flash_status(int32_t status) +{ + g_wt_rom_flash_last_status = (uint32_t)status; + return (status == WT_ROM_FLASH_STATUS_OK) ? WT_ROM_FLASH_OK : + WT_ROM_FLASH_ERR_ROM; +} + +int wt_rom_flash_init(void) +{ + const wt_rom_flash_api_t *api; + uint32_t sector = 0u; + uint32_t total = 0u; + uint32_t primask; + size_t i; + int ret = WT_ROM_FLASH_OK; + + if (g_rom_flash_ready != 0) { + return WT_ROM_FLASH_OK; + } + api = wt_rom_flash_api(); + if (api == NULL || api->init == NULL || api->erase == NULL || + api->program == NULL || api->verify_erase == NULL || + api->get_property == NULL) { + return WT_ROM_FLASH_ERR_ROM; + } + for (i = 0u; i < WT_ROM_FLASH_CONFIG_WORDS; i++) { + g_rom_flash_config[i] = 0u; + } + primask = wt_rom_flash_irq_mask(); + ret = wt_rom_flash_status(api->init(g_rom_flash_config)); + if (ret == WT_ROM_FLASH_OK) { + ret = wt_rom_flash_status(api->get_property(g_rom_flash_config, + WT_ROM_FLASH_PROP_SECTOR, + §or)); + } + if (ret == WT_ROM_FLASH_OK) { + ret = wt_rom_flash_status(api->get_property(g_rom_flash_config, + WT_ROM_FLASH_PROP_TOTAL, + &total)); + } + wt_rom_flash_irq_restore(primask); + /* The port's map is only valid on the geometry the ROM reports. */ + if (ret == WT_ROM_FLASH_OK && + (sector != WT_FLASH_SECTOR_SIZE || total != WT_FLASH_SIZE)) { + ret = WT_ROM_FLASH_ERR_GEOMETRY; + } + if (ret == WT_ROM_FLASH_OK) { + g_rom_flash_ready = 1; + } + return ret; +} + +int wt_rom_flash_blank(uint32_t offset, uint32_t size) +{ + const volatile uint8_t *view; + uint32_t i; + + if (offset > WT_FLASH_SIZE || size > WT_FLASH_SIZE - offset) { + return WT_ROM_FLASH_ERR_ARGS; + } + view = wt_rom_flash_view(offset); + for (i = 0u; i < size; i++) { + if (view[i] != 0xFFu) { + return WT_ROM_FLASH_ERR_NOTBLANK; + } + } + return WT_ROM_FLASH_OK; +} + +int wt_rom_flash_program(uint32_t offset, const uint8_t *data, uint32_t size) +{ + uint32_t primask; + int ret; + + if (data == NULL || + !wt_rom_flash_range_ok(offset, size, WT_FLASH_PHRASE_SIZE)) { + return WT_ROM_FLASH_ERR_ARGS; + } + ret = wt_rom_flash_init(); + if (ret == WT_ROM_FLASH_OK) { + ret = wt_rom_flash_blank(offset, size); + } + if (ret == WT_ROM_FLASH_OK) { + primask = wt_rom_flash_irq_mask(); + ret = wt_rom_flash_status(wt_rom_flash_api()->program( + g_rom_flash_config, offset, (uint8_t *)(uintptr_t)data, size)); + wt_rom_flash_cache_clear(); + wt_rom_flash_irq_restore(primask); + } + return ret; +} + +int wt_rom_flash_erase(uint32_t offset, uint32_t size) +{ + const wt_rom_flash_api_t *api; + uint32_t primask; + int ret; + + if (!wt_rom_flash_range_ok(offset, size, WT_FLASH_SECTOR_SIZE)) { + return WT_ROM_FLASH_ERR_ARGS; + } + ret = wt_rom_flash_init(); + if (ret == WT_ROM_FLASH_OK) { + api = wt_rom_flash_api(); + primask = wt_rom_flash_irq_mask(); + ret = wt_rom_flash_status(api->erase(g_rom_flash_config, offset, size, + WT_ROM_FLASH_ERASE_KEY)); + if (ret == WT_ROM_FLASH_OK) { + ret = wt_rom_flash_status(api->verify_erase(g_rom_flash_config, + offset, size)); + } + wt_rom_flash_cache_clear(); + wt_rom_flash_irq_restore(primask); + } + return ret; +} + +uint32_t wt_rom_flash_last_status(void) +{ + return g_wt_rom_flash_last_status; +} diff --git a/port/mcxn947/rom_flash.h b/port/mcxn947/rom_flash.h new file mode 100644 index 00000000..1642ec6d --- /dev/null +++ b/port/mcxn947/rom_flash.h @@ -0,0 +1,41 @@ +/* rom_flash.h + * + * 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 . + */ + +#ifndef WOLFTRUST_MCXN947_ROM_FLASH_H +#define WOLFTRUST_MCXN947_ROM_FLASH_H + +#include + +/* Internal flash through the boot ROM's flash API. Offsets are flash offsets + * (the Secure alias less 0x10000000); the privileged SPM is the only caller. + * A phrase is programmed once per erase: program refuses a non-blank phrase. */ +#define WT_ROM_FLASH_OK 0 +#define WT_ROM_FLASH_ERR_ARGS -1 +#define WT_ROM_FLASH_ERR_ROM -2 +#define WT_ROM_FLASH_ERR_NOTBLANK -3 +#define WT_ROM_FLASH_ERR_GEOMETRY -4 + +int wt_rom_flash_init(void); +int wt_rom_flash_program(uint32_t offset, const uint8_t *data, uint32_t size); +int wt_rom_flash_erase(uint32_t offset, uint32_t size); +int wt_rom_flash_blank(uint32_t offset, uint32_t size); +uint32_t wt_rom_flash_last_status(void); + +#endif /* WOLFTRUST_MCXN947_ROM_FLASH_H */ diff --git a/port/mcxn947/secure.ld b/port/mcxn947/secure.ld new file mode 100644 index 00000000..5fb90d5d --- /dev/null +++ b/port/mcxn947/secure.ld @@ -0,0 +1,134 @@ +/* secure.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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +ENTRY(Reset_Handler) + +MEMORY { + FLASH (rx) : ORIGIN = (WT_SECURE_FLASH_ORIGIN + WT_SECURE_IMAGE_HEADER_SIZE), + LENGTH = (WT_SECURE_FLASH_SIZE - WT_SECURE_IMAGE_HEADER_SIZE) + INCLUDE secure_l3_memory.ld +} + +INCLUDE secure_l3_symbols.ld +_sidata = LOADADDR(.data); + +SECTIONS { + .vectors (WT_SECURE_FLASH_ORIGIN + WT_SECURE_IMAGE_HEADER_SIZE) : { + KEEP(*(.vectors)) + } > FLASH + + /* Linker-synthesized SG veneers land here (--cmse-implib) in the image's + * second 1 KiB, the SAU NSC window (WT_NSC_* in memory_map.h); the veneer + * bodies live in .text. The dot assignment keeps the section address + * assigned even when no input object carries .gnu.sgstubs content. */ + .gnu.sgstubs (WT_SECURE_FLASH_ORIGIN + WT_SECURE_IMAGE_HEADER_SIZE + 0x400) : { + *(.gnu.sgstubs) + *(.gnu.sgstubs*) + . = ALIGN(32); + } > FLASH + + .nsc ALIGN(32) : { + KEEP(*(.nsc*)) + } > FLASH + + .text (WT_SECURE_FLASH_ORIGIN + WT_SECURE_IMAGE_HEADER_SIZE + 0x800) : { + _s_secure_text = .; + *(.text*) + *(.glue_7) + *(.glue_7t) + . = ALIGN(32); + _e_secure_text = .; + } > FLASH + + /* Constant data is readable, never executable (WT-FFM-0010): the SP thread + * MPU tables grant [flash, _e_secure_text) RX and the rest of the image + * window read-only XN, so no partition can execute from constant data. */ + .rodata (READONLY) : { + . = ALIGN(4); + KEEP(*(.wt_guest_meas)) + /* wolfCrypt globals that are never written after link stay read-only so + * the crypto partition sees them without owning writable state. */ + *(.data.always_prefetch* .bss.always_prefetch*) + *(.data.sha256DrbgDisabled* .bss.sha256DrbgDisabled*) + *(.rodata*) + *(.ARM.extab* .gnu.linkonce.armextab.*) + *(.ARM.exidx* .gnu.linkonce.armexidx.*) + *(.eh_frame) + } > FLASH + + ASSERT(WT_SECURE_IMAGE_HEADER_SIZE < WT_SECURE_FLASH_SIZE, + "secure image header consumes secure flash region") + ASSERT(. <= (WT_SECURE_FLASH_ORIGIN + WT_SECURE_FLASH_SIZE), + "secure image overflows secure flash region") + ASSERT(SIZEOF(.gnu.sgstubs) + SIZEOF(.nsc) <= 0x400, "NSC window overflow") + + /* Claim the conformance partitions' .data/.bss before the general rules so + * they land in CONFDATA, not shared SPM RAM. Loaded from flash and copied by + * Reset_Handler alongside .data; empty (zero size) in non-conformance builds. + * The server and driver partitions each get a 32-byte-aligned private band + * the SVC gate denies to every other partition; their .bss lands in the + * loaded section so each band stays one contiguous MPU carve. */ + .conf_data : { + . = ALIGN(4); + _sconfdata = .; + EXCLUDE_FILE(*conf_sec_test_supp_*.o *conf_sec_driver_partition.o *conf_sec_pal_driver_intf.o) *conf_sec_*.o(.data .data.*) + . = ALIGN(32); + _s_conf_server_data = .; + *conf_sec_test_supp_*.o(.data .data.* .bss .bss.* COMMON) + . = ALIGN(32); + _e_conf_server_data = .; + _s_conf_driver_data = .; + *conf_sec_driver_partition.o(.data .data.* .bss .bss.* COMMON) + *conf_sec_pal_driver_intf.o(.data .data.* .bss .bss.* COMMON) + . = ALIGN(32); + _e_conf_driver_data = .; + _econfdata = .; + } > CONFDATA AT > FLASH + _siconfdata = LOADADDR(.conf_data); + + .conf_bss (NOLOAD) : { + . = ALIGN(4); + _sconfbss = .; + EXCLUDE_FILE(*conf_sec_test_supp_*.o *conf_sec_driver_partition.o *conf_sec_pal_driver_intf.o) *conf_sec_*.o(.bss .bss.* COMMON) + . = ALIGN(4); + _econfbss = .; + } > CONFDATA + + INCLUDE secure_l3_bands.ld + + .data : { + _sdata = .; + *(.data*) + _edata = .; + } > RAM AT > FLASH + ASSERT(LOADADDR(.data) + SIZEOF(.data) <= + (WT_SECURE_FLASH_ORIGIN + WT_SECURE_FLASH_SIZE), + "secure image load image overflows secure flash region") + + .bss (NOLOAD) : { + _sbss = .; + *(.bss*) + *(COMMON) + _ebss = .; + } > RAM + + INCLUDE secure_l3_tail.ld +} diff --git a/port/mcxn947/secure_owners.txt b/port/mcxn947/secure_owners.txt new file mode 100644 index 00000000..e1815063 --- /dev/null +++ b/port/mcxn947/secure_owners.txt @@ -0,0 +1,3 @@ +# MCXN947 objects, appended to tools/secure_owners.txt by the secure link. +sec_platform_mcxn947.o spm +wt_sec_rom_flash.o spm From 7d64a928eeb7a160a3c5ed52a4486cc6d50d4f7e Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 23 Oct 2026 21:46:26 +0100 Subject: [PATCH 03/45] Prove the MCXN947 flash phrase mapping on the host --- tests/host/Makefile | 1 + tests/host/mcxn947_flash/Makefile | 97 ++++ tests/host/mcxn947_flash/main.c | 660 ++++++++++++++++++++++++ tests/host/mcxn947_flash/mock_rom_api.h | 34 ++ 4 files changed, 792 insertions(+) create mode 100644 tests/host/mcxn947_flash/Makefile create mode 100644 tests/host/mcxn947_flash/main.c create mode 100644 tests/host/mcxn947_flash/mock_rom_api.h diff --git a/tests/host/Makefile b/tests/host/Makefile index 95a9108b..127053cd 100644 --- a/tests/host/Makefile +++ b/tests/host/Makefile @@ -33,6 +33,7 @@ endif UNIT_SUITES := domain manifest lifecycle monitor guest_verify rollback sp_recovery \ fabric_windows periph irq_claim rt700_trng rt700_xspi rt700_lifecycle \ + mcxn947_flash \ ipc ffm spm \ vnet wolfhsm_loopback wolfhsm_relay native_wire native_wire_client \ keystore_isolation \ diff --git a/tests/host/mcxn947_flash/Makefile b/tests/host/mcxn947_flash/Makefile new file mode 100644 index 00000000..7161fece --- /dev/null +++ b/tests/host/mcxn947_flash/Makefile @@ -0,0 +1,97 @@ +# 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 := ../../.. +WOLFHSM := $(ROOT)/lib/wolfHSM + +CC ?= cc +BUILD_DIR ?= build +# The port's unit (16, the phrase) is the default run; `run` repeats the +# suite with wolfHSM's 8-byte default to show the hazard it would carry. +WT_FLASH_UNIT ?= 16 + +CFLAGS := \ + -DWOLFHSM_CFG_NO_CRYPTO \ + -DWOLFHSM_CFG_NO_SYS_TIME \ + -DWOLFHSM_CFG_FLASH_UNIT_SIZE=$(WT_FLASH_UNIT) \ + -I$(ROOT)/include \ + -I$(ROOT)/port/mcxn947 \ + -I$(WOLFHSM) \ + -include mock_rom_api.h \ + -std=c99 -O0 -g -Wall -Wextra -pedantic +CFLAGS += $(EXTRA_CFLAGS) + +_PORT_SRCS := $(ROOT)/port/mcxn947/rom_flash.c +_WH_SRCS := \ + $(WOLFHSM)/src/wh_nvm.c \ + $(WOLFHSM)/src/wh_nvm_flash.c \ + $(WOLFHSM)/src/wh_flash_unit.c \ + $(WOLFHSM)/src/wh_utils.c +_TEST_SRCS := main.c + +_PORT_OBJS := $(patsubst %.c,wt_%.o,$(notdir $(_PORT_SRCS))) +_WH_OBJS := $(patsubst %.c,wh_%.o,$(notdir $(_WH_SRCS))) +_TEST_OBJS := $(patsubst %.c,%.o,$(notdir $(_TEST_SRCS))) + +ALL_OBJS := $(addprefix $(BUILD_DIR)/,$(_PORT_OBJS) $(_WH_OBJS) $(_TEST_OBJS)) +TEST_BIN := $(BUILD_DIR)/test_mcxn947_flash + +.PHONY: all run run-one compilers sanitize valgrind clean + +all: $(TEST_BIN) + +$(BUILD_DIR): + mkdir -p $@ + +$(BUILD_DIR)/wt_%.o: $(ROOT)/port/mcxn947/%.c mock_rom_api.h | $(BUILD_DIR) + $(CC) $(CFLAGS) -c -o $@ $< + +$(BUILD_DIR)/wh_%.o: $(WOLFHSM)/src/%.c | $(BUILD_DIR) + $(CC) $(CFLAGS) -Wno-unused-parameter -c -o $@ $< + +$(BUILD_DIR)/%.o: %.c mock_rom_api.h | $(BUILD_DIR) + $(CC) $(CFLAGS) -c -o $@ $< + +$(TEST_BIN): $(ALL_OBJS) + $(CC) $(CFLAGS) $(EXTRA_LDFLAGS) -o $@ $(ALL_OBJS) + +run: $(TEST_BIN) + $(TEST_BIN) + $(MAKE) --no-print-directory run-one \ + BUILD_DIR=$(BUILD_DIR)/unit8 WT_FLASH_UNIT=8 + +run-one: $(TEST_BIN) + $(TEST_BIN) + +compilers: + $(MAKE) clean run CC=gcc BUILD_DIR=$(BUILD_DIR)/gcc + $(MAKE) clean run CC=clang BUILD_DIR=$(BUILD_DIR)/clang + +sanitize: + $(MAKE) clean run CC=clang \ + BUILD_DIR=$(BUILD_DIR)/sanitize \ + EXTRA_CFLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" \ + EXTRA_LDFLAGS="-fsanitize=address,undefined" + +valgrind: clean $(TEST_BIN) + valgrind --error-exitcode=1 --leak-check=full --show-leak-kinds=all \ + $(TEST_BIN) + +clean: + rm -rf $(BUILD_DIR) diff --git a/tests/host/mcxn947_flash/main.c b/tests/host/mcxn947_flash/main.c new file mode 100644 index 00000000..1a312d40 --- /dev/null +++ b/tests/host/mcxn947_flash/main.c @@ -0,0 +1,660 @@ +/* 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 . + */ + +/* Host suite for the MCXN947 ROM flash driver and the wolfHSM store on top + * of it. The model is the part's ECC flash: 16-byte phrases that bits can + * only be cleared in, and that a second program of (before an erase) leaves + * with an invalid ECC, counted here as a double program. The real driver + * (port/mcxn947/rom_flash.c) runs against a mock of the boot ROM's flash + * API tree; the wolfHSM NVM lifecycle then runs over it with the port's + * 16-byte unit, and, built with the 8-byte default, over a raw backend that + * shows two units sharing a phrase being written at different times. */ + +#include +#include +#include + +#include "memory_map.h" +#include "rom_flash.h" + +#include "wolfhsm/wh_error.h" +#include "wolfhsm/wh_flash.h" +#include "wolfhsm/wh_flash_unit.h" +#include "wolfhsm/wh_nvm.h" +#include "wolfhsm/wh_nvm_flash.h" + +/* Layouts mirrored from rom_flash.c (the driver's private view of the ROM). */ +typedef struct mock_flash_api { + uint32_t version; + int32_t (*init)(void *config); + int32_t (*erase)(void *config, uint32_t start, uint32_t length, + uint32_t key); + int32_t (*program)(void *config, uint32_t start, uint8_t *src, + uint32_t length); + int32_t (*verify_erase)(void *config, uint32_t start, uint32_t length); + int32_t (*verify_program)(void *config, uint32_t start, uint32_t length, + const uint8_t *expected, uint32_t *failed_address, + uint32_t *failed_data); + int32_t (*get_property)(void *config, uint32_t property, uint32_t *value); +} mock_flash_api_t; + +typedef struct mock_api_tree { + void (*run_bootloader)(void *arg); + uint32_t version; + const char *copyright; + uint32_t reserved0; + const mock_flash_api_t *flash; +} mock_api_tree_t; + +#define PHRASE WT_FLASH_PHRASE_SIZE +#define SECTOR WT_FLASH_SECTOR_SIZE +#define PHRASES (WT_FLASH_SIZE / PHRASE) +#define ERASE_KEY 0x6B65666Cu +#define STATUS_OK 0 +#define STATUS_ALIGN 101 +#define STATUS_KEY 107 +#define STATUS_COMPARE 117 +#define NVM_OFF (WT_HSM_NVM_FLASH_BASE_S - WT_FLASH_S_ALIAS_BASE) +#define NVM_SIZE WT_HSM_NVM_FLASH_SIZE + +static uint8_t g_flash[WT_FLASH_SIZE]; +static uint8_t g_programmed[PHRASES]; + +/* Model counters and knobs. */ +static uint32_t g_double_programs; +static uint32_t g_first_double_offset; +static uint32_t g_program_calls; +static uint32_t g_erase_calls; +static uint32_t g_verify_erase_calls; +static uint32_t g_init_calls; +static uint32_t g_prop_sector = SECTOR; +static uint32_t g_prop_total = WT_FLASH_SIZE; +static int32_t g_program_status = STATUS_OK; +static int32_t g_erase_status = STATUS_OK; + +static int g_failures; +static int g_checks; + +static void check(int cond, const char *name) +{ + g_checks++; + if (cond) { + printf("PASS: %s\n", name); + } + else { + printf("FAIL: %s\n", name); + g_failures++; + } +} + +/* Program as the array does: bits clear, and a phrase written twice since + * its erase no longer carries a valid ECC. */ +static void model_program(uint32_t start, const uint8_t *src, uint32_t len) +{ + uint32_t first = start / PHRASE; + uint32_t last = (start + len - 1u) / PHRASE; + uint32_t p; + uint32_t i; + + for (p = first; p <= last; p++) { + if (g_programmed[p] != 0u) { + if (g_double_programs == 0u) { + g_first_double_offset = p * PHRASE; + } + g_double_programs++; + } + g_programmed[p] = 1u; + } + for (i = 0u; i < len; i++) { + g_flash[start + i] &= src[i]; + } +} + +static void model_erase(uint32_t start, uint32_t len) +{ + uint32_t p; + + (void)memset(g_flash + start, 0xFF, len); + for (p = start / PHRASE; p < (start + len) / PHRASE; p++) { + g_programmed[p] = 0u; + } +} + +static void model_reset(void) +{ + (void)memset(g_flash, 0xFF, sizeof(g_flash)); + (void)memset(g_programmed, 0, sizeof(g_programmed)); + g_double_programs = 0u; + g_first_double_offset = 0u; +} + +static int32_t mock_init(void *config) +{ + uint32_t *words = (uint32_t *)config; + + g_init_calls++; + words[0] = 0u; + words[1] = g_prop_total; + words[2] = 2u; + words[3] = 128u; + words[4] = g_prop_sector; + return STATUS_OK; +} + +static int32_t mock_erase(void *config, uint32_t start, uint32_t length, + uint32_t key) +{ + (void)config; + g_erase_calls++; + if (key != ERASE_KEY) { + return STATUS_KEY; + } + if ((start % 4u) != 0u || (length % 4u) != 0u || + start + length > WT_FLASH_SIZE) { + return STATUS_ALIGN; + } + if (g_erase_status != STATUS_OK) { + return g_erase_status; + } + model_erase(start, length); + return STATUS_OK; +} + +static int32_t mock_program(void *config, uint32_t start, uint8_t *src, + uint32_t length) +{ + (void)config; + g_program_calls++; + if ((start % 4u) != 0u || (length % 4u) != 0u || + start + length > WT_FLASH_SIZE) { + return STATUS_ALIGN; + } + if (g_program_status != STATUS_OK) { + return g_program_status; + } + model_program(start, src, length); + return STATUS_OK; +} + +static int32_t mock_verify_erase(void *config, uint32_t start, uint32_t length) +{ + uint32_t i; + + (void)config; + g_verify_erase_calls++; + for (i = 0u; i < length; i++) { + if (g_flash[start + i] != 0xFFu) { + return STATUS_COMPARE; + } + } + return STATUS_OK; +} + +static int32_t mock_verify_program(void *config, uint32_t start, + uint32_t length, const uint8_t *expected, + uint32_t *failed_address, + uint32_t *failed_data) +{ + (void)config; + (void)failed_address; + (void)failed_data; + return (memcmp(g_flash + start, expected, length) == 0) ? STATUS_OK : + STATUS_COMPARE; +} + +static int32_t mock_get_property(void *config, uint32_t property, + uint32_t *value) +{ + (void)config; + if (property == 0u) { + *value = g_prop_sector; + } + else if (property == 1u) { + *value = g_prop_total; + } + else { + return 106; + } + return STATUS_OK; +} + +static const mock_flash_api_t g_mock_api = { + 0x46010000u, mock_init, mock_erase, mock_program, mock_verify_erase, + mock_verify_program, mock_get_property +}; +static const mock_api_tree_t g_mock_tree = { + NULL, 0x4B010000u, "mock", 0u, &g_mock_api +}; + +uintptr_t wt_mock_rom_tree(void) +{ + return (uintptr_t)&g_mock_tree; +} + +uintptr_t wt_mock_flash_base(void) +{ + return (uintptr_t)g_flash; +} + +static void test_driver(void) +{ + uint8_t phrase[PHRASE]; + uint8_t two[2u * PHRASE]; + uint32_t calls; + int rc; + + model_reset(); + (void)memset(phrase, 0x5A, sizeof(phrase)); + (void)memset(two, 0xC3, sizeof(two)); + + /* The ROM's geometry must be the one the map was written for. */ + g_prop_sector = 0x1000u; + check(wt_rom_flash_init() == WT_ROM_FLASH_ERR_GEOMETRY, + "init refuses a ROM reporting another sector size"); + g_prop_sector = SECTOR; + check(wt_rom_flash_init() == WT_ROM_FLASH_OK && g_init_calls == 2u, + "init accepts the 8 KiB sector, 2 MiB geometry"); + check(wt_rom_flash_init() == WT_ROM_FLASH_OK && g_init_calls == 2u, + "init is idempotent"); + + check(wt_rom_flash_program(NVM_OFF + 8u, phrase, PHRASE) == + WT_ROM_FLASH_ERR_ARGS, "program refuses a phrase-misaligned offset"); + check(wt_rom_flash_program(NVM_OFF, phrase, 8u) == WT_ROM_FLASH_ERR_ARGS, + "program refuses a sub-phrase length"); + check(wt_rom_flash_program(NVM_OFF, NULL, PHRASE) == WT_ROM_FLASH_ERR_ARGS, + "program refuses a null source"); + check(wt_rom_flash_program(WT_FLASH_SIZE - PHRASE, two, sizeof(two)) == + WT_ROM_FLASH_ERR_ARGS, "program refuses a range past the array"); + check(g_program_calls == 0u, "refused programs never reach the ROM"); + + check(wt_rom_flash_program(NVM_OFF, two, sizeof(two)) == WT_ROM_FLASH_OK && + memcmp(g_flash + NVM_OFF, two, sizeof(two)) == 0, + "two blank phrases program in one ROM call"); + calls = g_program_calls; + rc = wt_rom_flash_program(NVM_OFF + PHRASE, phrase, PHRASE); + check(rc == WT_ROM_FLASH_ERR_NOTBLANK && g_program_calls == calls, + "a programmed phrase is refused before the ROM is called"); + check(g_double_programs == 0u, "no phrase was programmed twice"); + check(memcmp(g_flash + NVM_OFF + PHRASE, two, PHRASE) == 0, + "the refused write left the phrase as it was"); + check(wt_rom_flash_blank(NVM_OFF, PHRASE) == WT_ROM_FLASH_ERR_NOTBLANK && + wt_rom_flash_blank(NVM_OFF + 2u * PHRASE, PHRASE) == WT_ROM_FLASH_OK, + "blank check reads the array through the view"); + + check(wt_rom_flash_erase(NVM_OFF + PHRASE, SECTOR) == WT_ROM_FLASH_ERR_ARGS, + "erase refuses a sector-misaligned offset"); + check(wt_rom_flash_erase(NVM_OFF, PHRASE) == WT_ROM_FLASH_ERR_ARGS, + "erase refuses a sub-sector length"); + check(g_erase_calls == 0u, "refused erases never reach the ROM"); + check(wt_rom_flash_erase(NVM_OFF, SECTOR) == WT_ROM_FLASH_OK && + g_verify_erase_calls == 1u && + wt_rom_flash_blank(NVM_OFF, SECTOR) == WT_ROM_FLASH_OK, + "sector erase runs the ROM erase and its verify"); + check(wt_rom_flash_program(NVM_OFF + PHRASE, phrase, PHRASE) == + WT_ROM_FLASH_OK && g_double_programs == 0u, + "an erased phrase programs again, once"); + + g_program_status = 103; + check(wt_rom_flash_program(NVM_OFF + 4u * PHRASE, phrase, PHRASE) == + WT_ROM_FLASH_ERR_ROM && wt_rom_flash_last_status() == 103u, + "a ROM program failure is reported with its status"); + g_program_status = STATUS_OK; + g_erase_status = 105; + check(wt_rom_flash_erase(NVM_OFF, SECTOR) == WT_ROM_FLASH_ERR_ROM && + wt_rom_flash_last_status() == 105u, + "a ROM erase failure is reported with its status"); + g_erase_status = STATUS_OK; +} + +/* The port backend's rules (port/mcxn947/hsm_flash.c) over the real driver: + * a 16-byte program unit, sector erases, reads through the alias. */ +static int port_init(void *context, const void *config) +{ + (void)context; + (void)config; + return WH_ERROR_OK; +} + +static int port_cleanup(void *context) +{ + (void)context; + return WH_ERROR_OK; +} + +static uint32_t port_partition_size(void *context) +{ + (void)context; + return SECTOR; +} + +static int port_lock(void *context, uint32_t offset, uint32_t size) +{ + (void)context; + (void)offset; + (void)size; + return WH_ERROR_OK; +} + +static int port_range_ok(uint32_t offset, uint32_t size) +{ + return (offset <= NVM_SIZE && size <= NVM_SIZE - offset) ? 1 : 0; +} + +static int port_read(void *context, uint32_t offset, uint32_t size, + uint8_t *data) +{ + (void)context; + if (!port_range_ok(offset, size)) { + return WH_ERROR_BADARGS; + } + (void)memcpy(data, g_flash + NVM_OFF + offset, size); + return WH_ERROR_OK; +} + +static int port_result(int rc) +{ + if (rc == WT_ROM_FLASH_OK) { + return WH_ERROR_OK; + } + if (rc == WT_ROM_FLASH_ERR_NOTBLANK) { + return WH_ERROR_NOTBLANK; + } + if (rc == WT_ROM_FLASH_ERR_ARGS) { + return WH_ERROR_BADARGS; + } + return WH_ERROR_ABORTED; +} + +static int port_program(void *context, uint32_t offset, uint32_t size, + const uint8_t *data) +{ + (void)context; + if (!port_range_ok(offset, size) || (offset % PHRASE) != 0u || + (size % PHRASE) != 0u) { + return WH_ERROR_BADARGS; + } + if (size == 0u) { + return WH_ERROR_OK; + } + return port_result(wt_rom_flash_program(NVM_OFF + offset, data, size)); +} + +static int port_erase(void *context, uint32_t offset, uint32_t size) +{ + (void)context; + if (!port_range_ok(offset, size) || (offset % SECTOR) != 0u || + (size % SECTOR) != 0u) { + return WH_ERROR_BADARGS; + } + if (size == 0u) { + return WH_ERROR_OK; + } + return port_result(wt_rom_flash_erase(NVM_OFF + offset, size)); +} + +static int port_verify(void *context, uint32_t offset, uint32_t size, + const uint8_t *data) +{ + (void)context; + if (!port_range_ok(offset, size)) { + return WH_ERROR_BADARGS; + } + return (memcmp(g_flash + NVM_OFF + offset, data, size) == 0) ? + WH_ERROR_OK : WH_ERROR_NOTVERIFIED; +} + +static int port_blank_check(void *context, uint32_t offset, uint32_t size) +{ + int rc; + + (void)context; + if (!port_range_ok(offset, size)) { + return WH_ERROR_BADARGS; + } + rc = wt_rom_flash_blank(NVM_OFF + offset, size); + if (rc == WT_ROM_FLASH_ERR_NOTBLANK) { + return WH_ERROR_NOTBLANK; + } + return (rc == WT_ROM_FLASH_OK) ? WH_ERROR_OK : WH_ERROR_BADARGS; +} + +static const whFlashCb g_port_cb = { + .Init = port_init, + .Cleanup = port_cleanup, + .PartitionSize = port_partition_size, + .WriteLock = port_lock, + .WriteUnlock = port_lock, + .Read = port_read, + .Program = port_program, + .Erase = port_erase, + .Verify = port_verify, + .BlankCheck = port_blank_check, +}; + +/* A backend that programs whatever unit it is handed, as a port that took + * the wolfHSM unit for the flash's own would. */ +static int raw_program(void *context, uint32_t offset, uint32_t size, + const uint8_t *data) +{ + (void)context; + if (!port_range_ok(offset, size) || (offset % 4u) != 0u || + (size % 4u) != 0u) { + return WH_ERROR_BADARGS; + } + model_program(NVM_OFF + offset, data, size); + return WH_ERROR_OK; +} + +static int raw_erase(void *context, uint32_t offset, uint32_t size) +{ + (void)context; + if (!port_range_ok(offset, size) || (offset % SECTOR) != 0u || + (size % SECTOR) != 0u) { + return WH_ERROR_BADARGS; + } + model_erase(NVM_OFF + offset, size); + return WH_ERROR_OK; +} + +static int raw_blank_check(void *context, uint32_t offset, uint32_t size) +{ + uint32_t i; + + (void)context; + if (!port_range_ok(offset, size)) { + return WH_ERROR_BADARGS; + } + for (i = 0u; i < size; i++) { + if (g_flash[NVM_OFF + offset + i] != 0xFFu) { + return WH_ERROR_NOTBLANK; + } + } + return WH_ERROR_OK; +} + +static const whFlashCb g_raw_cb = { + .Init = port_init, + .Cleanup = port_cleanup, + .PartitionSize = port_partition_size, + .WriteLock = port_lock, + .WriteUnlock = port_lock, + .Read = port_read, + .Program = raw_program, + .Erase = raw_erase, + .Verify = port_verify, + .BlankCheck = raw_blank_check, +}; + +#define OBJECTS 20u +#define OBJECT_MAX 300u +#define ROUNDS 3u + +static whNvmFlashContext g_nf_ctx; +static whNvmFlashConfig g_nf_cfg; +static whNvmContext g_nvm; +static whNvmConfig g_nvm_cfg; +static whNvmCb g_nvm_cb = WH_NVM_FLASH_CB; +static uint8_t g_pattern[OBJECT_MAX]; + +static void fill_object(uint8_t *out, uint32_t id, uint32_t len) +{ + uint32_t i; + + for (i = 0u; i < len; i++) { + out[i] = (uint8_t)(id * 7u + i * 13u + 1u); + } +} + +static int nvm_open(const whFlashCb *cb) +{ + (void)memset(&g_nf_ctx, 0, sizeof(g_nf_ctx)); + (void)memset(&g_nvm, 0, sizeof(g_nvm)); + g_nf_cfg.cb = cb; + g_nf_cfg.context = NULL; + g_nf_cfg.config = NULL; + g_nvm_cfg.cb = &g_nvm_cb; + g_nvm_cfg.context = &g_nf_ctx; + g_nvm_cfg.config = &g_nf_cfg; + return wh_Nvm_Init(&g_nvm, &g_nvm_cfg); +} + +/* Fill, read back, destroy, refill until the store compacts into its other + * partition, then reopen the store as a reboot would and read everything + * back. Every sector erase after the open is one such compaction. Returns 0 + * when every object read back as written. */ +static int nvm_lifecycle(const whFlashCb *cb, uint32_t *compactions) +{ + whNvmMetadata meta; + uint8_t readback[OBJECT_MAX]; + whNvmId victims[OBJECTS / 2u]; + uint32_t round; + uint32_t i; + uint32_t len; + uint32_t erases_at_open; + int ret; + + *compactions = 0u; + ret = nvm_open(cb); + erases_at_open = g_erase_calls; + for (round = 0u; ret == WH_ERROR_OK && round < ROUNDS; round++) { + for (i = 0u; ret == WH_ERROR_OK && i < OBJECTS; i++) { + len = 1u + ((i * 37u + round * 11u) % OBJECT_MAX); + (void)memset(&meta, 0, sizeof(meta)); + meta.id = (whNvmId)(0x100u + i); + meta.access = WH_NVM_ACCESS_ANY; + meta.flags = WH_NVM_FLAGS_NONE; + meta.len = (whNvmSize)len; + fill_object(g_pattern, meta.id + round, len); + ret = wh_Nvm_AddObjectWithReclaim(&g_nvm, &meta, (whNvmSize)len, + g_pattern); + } + for (i = 0u; ret == WH_ERROR_OK && i < OBJECTS; i++) { + len = 1u + ((i * 37u + round * 11u) % OBJECT_MAX); + fill_object(g_pattern, 0x100u + i + round, len); + (void)memset(readback, 0, sizeof(readback)); + ret = wh_Nvm_Read(&g_nvm, (whNvmId)(0x100u + i), 0u, + (whNvmSize)len, readback); + if (ret == WH_ERROR_OK && memcmp(readback, g_pattern, len) != 0) { + ret = WH_ERROR_NOTVERIFIED; + } + } + for (i = 0u; i < OBJECTS / 2u; i++) { + victims[i] = (whNvmId)(0x100u + 2u * i); + } + if (ret == WH_ERROR_OK) { + ret = wh_Nvm_DestroyObjects(&g_nvm, OBJECTS / 2u, victims); + } + } + *compactions = g_erase_calls - erases_at_open; + if (ret == WH_ERROR_OK) { + ret = wh_Nvm_Cleanup(&g_nvm); + } + if (ret == WH_ERROR_OK) { + ret = nvm_open(cb); + } + for (i = 1u; ret == WH_ERROR_OK && i < OBJECTS; i += 2u) { + len = 1u + ((i * 37u + (ROUNDS - 1u) * 11u) % OBJECT_MAX); + fill_object(g_pattern, 0x100u + i + ROUNDS - 1u, len); + (void)memset(readback, 0, sizeof(readback)); + ret = wh_Nvm_Read(&g_nvm, (whNvmId)(0x100u + i), 0u, (whNvmSize)len, + readback); + if (ret == WH_ERROR_OK && memcmp(readback, g_pattern, len) != 0) { + ret = WH_ERROR_NOTVERIFIED; + } + } + return ret; +} + +static void test_store(void) +{ + uint32_t compactions = 0u; + int ret; + + model_reset(); + g_program_calls = 0u; + g_erase_calls = 0u; +#if WOLFHSM_CFG_FLASH_UNIT_SIZE == 16 + ret = nvm_lifecycle(&g_port_cb, &compactions); + check(ret == WH_ERROR_OK, + "16-byte units: three fill/read/destroy rounds and a reopen"); + check(compactions >= 1u, + "16-byte units: the store compacted into its other partition"); + check(g_program_calls > 100u, + "16-byte units: the lifecycle programmed many phrases"); + printf(" programs=%u compactions=%u double=%u\n", + (unsigned)g_program_calls, (unsigned)compactions, + (unsigned)g_double_programs); + check(g_double_programs == 0u, + "16-byte units: no phrase was programmed twice between erases"); + /* The unit, not the port's guard, is what keeps every program whole: + * a backend with no phrase rule stays clean at 16 bytes. */ + model_reset(); + ret = nvm_lifecycle(&g_raw_cb, &compactions); + check(ret == WH_ERROR_OK && g_double_programs == 0u, + "16-byte units: the raw backend never writes a phrase twice"); +#else + ret = nvm_open(&g_port_cb); + check(ret != WH_ERROR_OK, + "8-byte units: the port backend refuses the sub-phrase unit"); + check(g_program_calls == 0u && g_double_programs == 0u, + "8-byte units: nothing reached the ROM through the port backend"); + model_reset(); + ret = nvm_lifecycle(&g_raw_cb, &compactions); + check(ret == WH_ERROR_OK, + "8-byte units: the raw backend completes the same lifecycle"); + printf(" compactions=%u double=%u first=0x%08X\n", + (unsigned)compactions, (unsigned)g_double_programs, + (unsigned)g_first_double_offset); + check(g_double_programs > 0u, + "8-byte units: two units sharing a phrase were written at " + "different times, which this flash would corrupt"); +#endif +} + +int main(void) +{ + printf("MCXN947 flash: WOLFHSM_CFG_FLASH_UNIT_SIZE=%d\n", + (int)WOLFHSM_CFG_FLASH_UNIT_SIZE); + test_driver(); + test_store(); + printf("%s: %d checks, %d failures\n", + (g_failures == 0) ? "PASS" : "FAIL", g_checks, g_failures); + return (g_failures == 0) ? 0 : 1; +} diff --git a/tests/host/mcxn947_flash/mock_rom_api.h b/tests/host/mcxn947_flash/mock_rom_api.h new file mode 100644 index 00000000..e45fc55c --- /dev/null +++ b/tests/host/mcxn947_flash/mock_rom_api.h @@ -0,0 +1,34 @@ +/* mock_rom_api.h + * + * 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 . + */ + +/* Force-included ahead of rom_flash.c: the boot ROM API tree and the flash + * read view come from the test's model instead of the part's addresses. */ +#ifndef WT_TEST_MOCK_ROM_API_H +#define WT_TEST_MOCK_ROM_API_H + +#include + +uintptr_t wt_mock_rom_tree(void); +uintptr_t wt_mock_flash_base(void); + +#define WT_ROM_API_TREE_ADDR (wt_mock_rom_tree()) +#define WT_ROM_FLASH_VIEW_BASE (wt_mock_flash_base()) + +#endif /* WT_TEST_MOCK_ROM_API_H */ From 32c79fb2fd9adf6a1607e58cd4a3784a1a9ee786 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 23 Oct 2026 22:04:32 +0100 Subject: [PATCH 04/45] Boot the MCXN947 chain under M33MU with both bare-metal guests --- .gitignore | 1 + tests/firmware/mcxn947-baremetal/Makefile | 109 +++ tests/firmware/mcxn947-baremetal/guest0.c | 307 +++++++++ tests/firmware/mcxn947-baremetal/guest0.ld | 57 ++ .../firmware/psa-guest/boards/mcxn947/board.h | 58 ++ .../psa-guest/boards/mcxn947/board.mk | 9 + tests/target/detect_mcxn947.sh | 31 + tests/target/lib/mcxn947_wolfboot.sh | 71 ++ tests/target/lib/scenario_matrix.py | 74 ++- tests/target/m33mu-mcxn947.patch | 240 +++++++ tests/target/run_mcxn947_m33mu.sh | 620 ++++++++++++++++++ tests/target/run_suite.sh | 10 +- tests/target/wolfboot-mcxn947-lifecycle.patch | 175 +++++ 13 files changed, 1748 insertions(+), 14 deletions(-) create mode 100644 tests/firmware/mcxn947-baremetal/Makefile create mode 100644 tests/firmware/mcxn947-baremetal/guest0.c create mode 100644 tests/firmware/mcxn947-baremetal/guest0.ld create mode 100644 tests/firmware/psa-guest/boards/mcxn947/board.h create mode 100644 tests/firmware/psa-guest/boards/mcxn947/board.mk create mode 100755 tests/target/detect_mcxn947.sh create mode 100644 tests/target/lib/mcxn947_wolfboot.sh create mode 100644 tests/target/m33mu-mcxn947.patch create mode 100755 tests/target/run_mcxn947_m33mu.sh create mode 100644 tests/target/wolfboot-mcxn947-lifecycle.patch diff --git a/.gitignore b/.gitignore index fe2d02fa..731b7823 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ tests/firmware/stm32h563/build/ tests/firmware/stm32h563-vnet/build/ tests/firmware/mimxrt700-baremetal/build/ +tests/firmware/mcxn947-baremetal/build/ tests/firmware/stm32h563-vnet/stm32h563_OTP.bin *.o /wolftrust diff --git a/tests/firmware/mcxn947-baremetal/Makefile b/tests/firmware/mcxn947-baremetal/Makefile new file mode 100644 index 00000000..fbee5bf0 --- /dev/null +++ b/tests/firmware/mcxn947-baremetal/Makefile @@ -0,0 +1,109 @@ +# Bare-metal Non-secure guest0 for the FRDM-MCXN947. Builds the wolfTrust +# Secure image (and its CMSE import library) through the top-level Makefile, +# then links guest0 against that import library so the WolfTrust_FFM_* veneers +# resolve. Image assembly and the emulator boot are driven by +# tests/target/run_mcxn947_m33mu.sh. +TOOLPREFIX ?= arm-none-eabi- +CC := $(TOOLPREFIX)gcc +OBJCOPY := $(TOOLPREFIX)objcopy +SIZE := $(TOOLPREFIX)size + +ROOT := ../../.. +TARGET ?= mcxn947 +ARCH ?= armv8m + +BUILD ?= build +TOP_BUILD_DIR := $(ROOT)/build +SECURE_CMSE_IMPLIB := $(TOP_BUILD_DIR)/secure_cmse_implib.o + +# The same guest links into both Non-secure windows (port/mcxn947/memory_map.h); +# the monitor refuses to start unless every configured guest has an image. +GUEST0_FLASH_ORIGIN ?= 0x00080000 +GUEST0_FLASH_LENGTH ?= 0x00080000 +GUEST0_RAM_ORIGIN ?= 0x20000000 +GUEST0_RAM_LENGTH ?= 0x00018000 +GUEST1_FLASH_ORIGIN ?= 0x00100000 +GUEST1_FLASH_LENGTH ?= 0x00040000 +GUEST1_RAM_ORIGIN ?= 0x20018000 +GUEST1_RAM_LENGTH ?= 0x00018000 + +CFLAGS := -mcpu=cortex-m33 -mthumb -mgeneral-regs-only -ffreestanding \ + -fno-builtin -nostdlib -Os -g -Wall -Wextra \ + -ffunction-sections -fdata-sections -mno-unaligned-access \ + -I$(ROOT)/include + +# WT_SAU_PROBE=1 builds guest0's isolation probe: it stores into guest1's RAM +# window, which the per-dispatch SAU window keeps Secure while guest0 runs. +# guest1 carries no probe; it is the peer that must keep running. +WT_SAU_PROBE ?= 0 +GUEST0_CFLAGS := -DGUEST_NAME=\"guest0\" +GUEST1_CFLAGS := -DGUEST_NAME=\"guest1\" +ifeq ($(WT_SAU_PROBE),1) +GUEST0_CFLAGS += -DWT_SAU_PROBE -DGUEST_PROBE_ADDR=0x20028000u \ + -DGUEST_PROBE_VALUE=0xDEADBEEFu +endif +# WT_PERIPH_NEG_PROBE=1 makes guest0, its MPU off, read the SPM's ELS through +# the Non-secure alias; the SAU must refuse it on every launch (periphneg). +WT_PERIPH_NEG_PROBE ?= 0 +ifeq ($(WT_PERIPH_NEG_PROBE),1) +GUEST0_CFLAGS += -DWT_PERIPH_NEG_PROBE -DGUEST_PERIPH_ADDR=0x40054000u +endif +# WT_GUEST_FAULT_PROBE=1 makes guest0 read Secure RAM on every launch, so the +# monitor spends its restart budget and quarantines it (restart). +WT_GUEST_FAULT_PROBE ?= 0 +ifeq ($(WT_GUEST_FAULT_PROBE),1) +GUEST0_CFLAGS += -DWT_GUEST_FAULT_PROBE -DGUEST_FAULT_ADDR=0x30031000u +endif + +CLIENT_OBJ := $(BUILD)/psa_ffm_client.o + +all: $(BUILD)/guest0.bin $(BUILD)/guest1.bin + +# The image's build mode comes from the environment; forcing one here would +# relink the image in another mode and drop the wolftrust.bin the runners sign. +$(SECURE_CMSE_IMPLIB): FORCE + $(MAKE) -C $(ROOT) ARCH=$(ARCH) TARGET=$(TARGET) TOOLPREFIX=$(TOOLPREFIX) \ + build/secure_cmse_implib.o + +$(BUILD)/guest0.o: guest0.c | $(BUILD) + $(CC) $(CFLAGS) $(GUEST0_CFLAGS) -c -o $@ $< + +$(BUILD)/guest1.o: guest0.c | $(BUILD) + $(CC) $(CFLAGS) $(GUEST1_CFLAGS) -c -o $@ $< + +$(BUILD)/psa_ffm_client.o: $(ROOT)/src/client/psa_ffm_client.c | $(BUILD) + $(CC) $(CFLAGS) -c -o $@ $< + +$(BUILD)/guest0.elf: $(BUILD)/guest0.o $(CLIENT_OBJ) $(SECURE_CMSE_IMPLIB) guest0.ld | $(BUILD) + $(CC) $(CFLAGS) \ + -Wl,-Tguest0.ld \ + -Wl,--defsym=GUEST_FLASH_ORIGIN=$(GUEST0_FLASH_ORIGIN) \ + -Wl,--defsym=GUEST_FLASH_LENGTH=$(GUEST0_FLASH_LENGTH) \ + -Wl,--defsym=GUEST_RAM_ORIGIN=$(GUEST0_RAM_ORIGIN) \ + -Wl,--defsym=GUEST_RAM_LENGTH=$(GUEST0_RAM_LENGTH) \ + -Wl,--gc-sections \ + -o $@ $(BUILD)/guest0.o $(CLIENT_OBJ) $(SECURE_CMSE_IMPLIB) -lgcc + $(SIZE) $@ + +$(BUILD)/guest1.elf: $(BUILD)/guest1.o $(CLIENT_OBJ) $(SECURE_CMSE_IMPLIB) guest0.ld | $(BUILD) + $(CC) $(CFLAGS) \ + -Wl,-Tguest0.ld \ + -Wl,--defsym=GUEST_FLASH_ORIGIN=$(GUEST1_FLASH_ORIGIN) \ + -Wl,--defsym=GUEST_FLASH_LENGTH=$(GUEST1_FLASH_LENGTH) \ + -Wl,--defsym=GUEST_RAM_ORIGIN=$(GUEST1_RAM_ORIGIN) \ + -Wl,--defsym=GUEST_RAM_LENGTH=$(GUEST1_RAM_LENGTH) \ + -Wl,--gc-sections \ + -o $@ $(BUILD)/guest1.o $(CLIENT_OBJ) $(SECURE_CMSE_IMPLIB) -lgcc + $(SIZE) $@ + +$(BUILD)/%.bin: $(BUILD)/%.elf + $(OBJCOPY) -O binary $< $@ + +$(BUILD): + mkdir -p $@ + +clean: + rm -rf $(BUILD) + +.PHONY: all clean FORCE +FORCE: diff --git a/tests/firmware/mcxn947-baremetal/guest0.c b/tests/firmware/mcxn947-baremetal/guest0.c new file mode 100644 index 00000000..31b4a657 --- /dev/null +++ b/tests/firmware/mcxn947-baremetal/guest0.c @@ -0,0 +1,307 @@ +/* guest0.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 . + */ + +/* Bare-metal Non-secure bring-up guest for the MCXN947 CPU0. wolfBoot + * authenticates the wolfTrust Secure image, which launches this guest in the + * Non-secure state; the guest drives the WolfTrust_FFM_* veneers to prove the + * Secure runtime booted and services a Non-secure PSA client. Progress lands + * in a debugger-readable mailbox at the base of guest RAM and, best effort, + * on LPUART4 (the FRDM board's MCU-Link console the first loader set up). */ + +#include +#include + +#include "psa/client.h" + +/* SERVICE_HSM from the platform manifest (port/mcxn947/manifest.json). */ +#define GUEST0_SERVICE_HSM_SID 4102u +#define GUEST0_SERVICE_ITS_SID 4099u +#define GUEST0_SERVICE_VERSION 1u + +/* One source builds both guests; the Makefile names each on the console. */ +#ifndef GUEST_NAME +#error "GUEST_NAME must name the guest" +#endif + +/* Debugger-readable status words at the base of the guest RAM window. */ +#define GUEST0_SIGNATURE 0x47543030u /* debugger tag, "guest0 ran" */ +#define GUEST0_STATUS_RUNNING 0x00000000u +#define GUEST0_STATUS_DONE 0x600D600Du +#define GUEST0_STATUS_FAIL 0xBAD00000u + +/* LPUART4 (LP_FLEXCOMM4) through its Non-secure alias. The first loader + * configured the clock, pins, and baud, so the guest only keeps the + * transmitter enabled, polls TDRE and writes DATA. */ +#define GUEST0_LPUART_BASE 0x400B4000u +#define GUEST0_LPUART_VERID (*(volatile uint32_t*)(GUEST0_LPUART_BASE + 0x00u)) +#define GUEST0_LPUART_STAT (*(volatile uint32_t*)(GUEST0_LPUART_BASE + 0x14u)) +#define GUEST0_LPUART_CTRL (*(volatile uint32_t*)(GUEST0_LPUART_BASE + 0x18u)) +#define GUEST0_LPUART_DATA (*(volatile uint32_t*)(GUEST0_LPUART_BASE + 0x1Cu)) +#define GUEST0_LPUART_STAT_TDRE 0x00800000u +#define GUEST0_LPUART_CTRL_TE_RE 0x000C0000u +#define GUEST0_UART_TX_POLL_LIMIT 100000u + +typedef struct wt_guest0_mailbox { + uint32_t signature; + uint32_t step; + uint32_t framework; + uint32_t hsm_version; + int32_t hsm_handle; + uint32_t status; + uint32_t console; + uint32_t probe; + uint32_t probe_read; + uint32_t beat; /* advances while the guest idles once done */ +} wt_guest0_mailbox_t; + +__attribute__((section(".shared"), used)) +volatile wt_guest0_mailbox_t g_guest0_mailbox; + +#if defined(WT_PERIPH_NEG_PROBE) +static void guest0_uart_puts(const char* s); + +/* A privileged guest switches off its own MPU and reads the SPM's ELS through + * the Non-secure alias. The SAU must fault the read; returning at all leaks. */ +static void guest0_periph_probe(volatile wt_guest0_mailbox_t* mb) +{ + *(volatile uint32_t*)0xE000ED94u = 0u; + __asm volatile("dsb\n isb" ::: "memory"); + mb->probe = 1u; + __asm volatile("dsb" ::: "memory"); + mb->probe_read = *(volatile const uint32_t*)GUEST_PERIPH_ADDR; + mb->probe = 3u; + guest0_uart_puts("wolfTrust MCXN947 " GUEST_NAME ": periph probe LEAKED\r\n"); +} +#endif + +#if defined(WT_SAU_PROBE) +/* Isolation probe: GUEST_PROBE_ADDR is the peer guest's RAM, Secure to the SAU + * while this guest runs. A privileged guest can switch off the Non-secure MPU + * wolfTrust programs for it, so attribution alone must stop the store. Latch: + * 1 attempted (the store faulted), 2 blocked, 3 leaked. */ +#define GUEST0_MPU_CTRL (*(volatile uint32_t*)0xE000ED94u) +#define GUEST0_PROBE_ATTEMPTED 1u +#define GUEST0_PROBE_BLOCKED 2u +#define GUEST0_PROBE_LEAKED 3u + +static void guest0_sau_probe(volatile wt_guest0_mailbox_t* mb) +{ + volatile uint32_t* target = (volatile uint32_t*)GUEST_PROBE_ADDR; + uint32_t readback; + + GUEST0_MPU_CTRL = 0u; + __asm volatile("dsb\n isb" ::: "memory"); + /* Latched only once the MPU is off, so 1 means the peer store was issued. */ + mb->probe = GUEST0_PROBE_ATTEMPTED; + __asm volatile("dsb" ::: "memory"); + *target = GUEST_PROBE_VALUE; + __asm volatile("dsb" ::: "memory"); + readback = *target; + mb->probe_read = readback; + if (readback == GUEST_PROBE_VALUE) { + mb->probe = GUEST0_PROBE_LEAKED; + } + else { + mb->probe = GUEST0_PROBE_BLOCKED; + } +} +#endif + +extern uint32_t _estack; +extern uint32_t _sidata; +extern uint32_t _sdata; +extern uint32_t _edata; +extern uint32_t _sbss; +extern uint32_t _ebss; + +void Reset_Handler(void); +static void Default_Handler(void); + +__attribute__((section(".vectors"), used)) +const uint32_t g_vectors[16] = { + (uint32_t)&_estack, + (uint32_t)Reset_Handler, + (uint32_t)Default_Handler, + (uint32_t)Default_Handler, + (uint32_t)Default_Handler, + (uint32_t)Default_Handler, + (uint32_t)Default_Handler, + (uint32_t)Default_Handler, + 0u, 0u, 0u, + (uint32_t)Default_Handler, + (uint32_t)Default_Handler, + 0u, + (uint32_t)Default_Handler, + (uint32_t)Default_Handler, +}; + +static void Default_Handler(void) +{ + for (;;) { + } +} + +static void guest0_uart_putc(char c) +{ + uint32_t t = GUEST0_UART_TX_POLL_LIMIT; + + while (((GUEST0_LPUART_STAT & GUEST0_LPUART_STAT_TDRE) == 0u) && + (t > 0u)) { + t--; + } + if (t != 0u) { + GUEST0_LPUART_DATA = (uint32_t)(uint8_t)c; + } +} + +static void guest0_uart_puts(const char* s) +{ + while (*s != '\0') { + guest0_uart_putc(*s); + s++; + } +} + +void Reset_Handler(void) +{ + volatile wt_guest0_mailbox_t* mb = &g_guest0_mailbox; + uint32_t* src; + uint32_t* dst; + uint32_t fw; + uint32_t tries; + psa_handle_t handle; + int ok = 1; + + src = &_sidata; + for (dst = &_sdata; dst < &_edata; ) { + *dst = *src; + dst++; + src++; + } + for (dst = &_sbss; dst < &_ebss; ) { + *dst = 0u; + dst++; + } + + mb->signature = GUEST0_SIGNATURE; + mb->step = 1u; + mb->framework = 0u; + mb->hsm_version = 0u; + mb->hsm_handle = 0; + mb->status = GUEST0_STATUS_RUNNING; + mb->console = GUEST0_LPUART_VERID; + mb->probe = 0u; + mb->probe_read = 0u; + mb->beat = 0u; + + GUEST0_LPUART_CTRL |= GUEST0_LPUART_CTRL_TE_RE; + guest0_uart_puts("wolfTrust MCXN947 " GUEST_NAME ": start\r\n"); + +#if defined(WT_GUEST_FAULT_PROBE) + /* A Non-secure read of Secure RAM faults on every launch. */ + mb->probe_read = *(volatile const uint32_t*)GUEST_FAULT_ADDR; +#endif + + fw = psa_framework_version(); + mb->framework = fw; + mb->step = 2u; + if (fw != PSA_FRAMEWORK_VERSION) { + ok = 0; + } + + mb->hsm_version = psa_version(GUEST0_SERVICE_HSM_SID); + mb->step = 3u; + + /* A failed connect to the mandatory service is retried: each attempt + * re-enters the relay, so a faulting relay spends its restart budget + * (spbudgetneg) instead of surviving on one entry per guest. */ + handle = PSA_NULL_HANDLE; + for (tries = 0u; tries < 4u; tries++) { + handle = psa_connect(GUEST0_SERVICE_HSM_SID, GUEST0_SERVICE_VERSION); + if (PSA_HANDLE_IS_VALID(handle)) { + break; + } + } + mb->hsm_handle = (int32_t)handle; + mb->step = 4u; + if (PSA_HANDLE_IS_VALID(handle)) { + psa_close(handle); + mb->step = 5u; + } + else { + ok = 0; + } + + if (ok != 0) { + mb->status = GUEST0_STATUS_DONE; + guest0_uart_puts("wolfTrust MCXN947 " GUEST_NAME + ": FF-M connect ok, done\r\n"); + } + else { + mb->status = GUEST0_STATUS_FAIL; + guest0_uart_puts("wolfTrust MCXN947 " GUEST_NAME ": FAIL\r\n"); + } + + /* Touch a second service so the storage SP wakes: the crossdomain and + * keystoreneg probes fault on its entry, so this connect failing is their + * verdict, never this guest's. */ + handle = psa_connect(GUEST0_SERVICE_ITS_SID, GUEST0_SERVICE_VERSION); + if (PSA_HANDLE_IS_VALID(handle)) { + psa_close(handle); + guest0_uart_puts("wolfTrust MCXN947 " GUEST_NAME + ": storage connect ok\r\n"); + } + +#if defined(WT_SAU_PROBE) + guest0_sau_probe(mb); +#endif +#if defined(WT_PERIPH_NEG_PROBE) + guest0_periph_probe(mb); +#endif + + for (;;) { + mb->beat++; + } +} + +/* The freestanding client core marshals its iovec through memset; -fno-builtin + * keeps these loops from being turned into calls to themselves. */ +void* memset(void* dst, int c, size_t n) +{ + uint8_t* p = (uint8_t*)dst; + size_t i; + + for (i = 0u; i < n; i++) { + p[i] = (uint8_t)c; + } + return dst; +} + +void* memcpy(void* dst, const void* src, size_t n) +{ + uint8_t* d = (uint8_t*)dst; + const uint8_t* s = (const uint8_t*)src; + size_t i; + + for (i = 0u; i < n; i++) { + d[i] = s[i]; + } + return dst; +} diff --git a/tests/firmware/mcxn947-baremetal/guest0.ld b/tests/firmware/mcxn947-baremetal/guest0.ld new file mode 100644 index 00000000..e561cd2f --- /dev/null +++ b/tests/firmware/mcxn947-baremetal/guest0.ld @@ -0,0 +1,57 @@ +/* guest0.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 = GUEST_FLASH_ORIGIN, LENGTH = GUEST_FLASH_LENGTH + RAM (rwx): ORIGIN = GUEST_RAM_ORIGIN, LENGTH = GUEST_RAM_LENGTH +} + +_estack = ORIGIN(RAM) + LENGTH(RAM); + +SECTIONS { + .shared ORIGIN(RAM) (NOLOAD) : { + *(.shared*) + } > RAM + + .vectors GUEST_FLASH_ORIGIN : { + KEEP(*(.vectors)) + } > FLASH + + .text : { + *(.text*) + *(.rodata*) + } > FLASH + + .data : { + _sdata = .; + *(.data*) + _edata = .; + } > RAM AT > FLASH + _sidata = LOADADDR(.data); + + .bss (NOLOAD) : { + _sbss = .; + *(.bss*) + *(COMMON) + _ebss = .; + } > RAM +} diff --git a/tests/firmware/psa-guest/boards/mcxn947/board.h b/tests/firmware/psa-guest/boards/mcxn947/board.h new file mode 100644 index 00000000..bcd38a8e --- /dev/null +++ b/tests/firmware/psa-guest/boards/mcxn947/board.h @@ -0,0 +1,58 @@ +/* board.h + * + * 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 . + */ + +/* MCXN947 (FRDM-MCXN947, CPU0) board seams for the portable PSA guest: the + * console is LPUART4 through its Non-secure alias, configured by the first + * loader, so the guest keeps the transmitter on, polls TDRE and writes DATA. */ + +#ifndef WOLFTRUST_PSA_GUEST_BOARD_H +#define WOLFTRUST_PSA_GUEST_BOARD_H + +#include + +#define GUEST_UART_BASE 0x400B4000u +#define GUEST_UART_STAT (*(volatile uint32_t*)(GUEST_UART_BASE + 0x14u)) +#define GUEST_UART_CTRL (*(volatile uint32_t*)(GUEST_UART_BASE + 0x18u)) +#define GUEST_UART_DATA (*(volatile uint32_t*)(GUEST_UART_BASE + 0x1Cu)) +#define GUEST_UART_STAT_TDRE 0x00800000u +#define GUEST_UART_CTRL_TE_RE 0x000C0000u +#define GUEST_UART_POLL_LIMIT 100000u + +static inline void guest_board_uart_init(void) +{ + GUEST_UART_CTRL |= GUEST_UART_CTRL_TE_RE; +} + +static inline void guest_board_uart_putc(char c) +{ + uint32_t t = GUEST_UART_POLL_LIMIT; + + while (((GUEST_UART_STAT & GUEST_UART_STAT_TDRE) == 0u) && (t > 0u)) { + t--; + } + if (t != 0u) { + GUEST_UART_DATA = (uint32_t)(uint8_t)c; + } +} + +/* Secure RAM the restart probe reads (WT_RAM_S_BASE in port/mcxn947). */ +#define GUEST_SECURE_RAM_ADDR 0x30031000u + +#endif /* WOLFTRUST_PSA_GUEST_BOARD_H */ diff --git a/tests/firmware/psa-guest/boards/mcxn947/board.mk b/tests/firmware/psa-guest/boards/mcxn947/board.mk new file mode 100644 index 00000000..ae642a17 --- /dev/null +++ b/tests/firmware/psa-guest/boards/mcxn947/board.mk @@ -0,0 +1,9 @@ +# MCXN947 Non-secure guest windows (port/mcxn947/memory_map.h). +GUEST0_FLASH_ORIGIN ?= 0x00080000 +GUEST0_FLASH_LENGTH ?= 0x00080000 +GUEST0_RAM_ORIGIN ?= 0x20000000 +GUEST0_RAM_LENGTH ?= 0x00018000 +GUEST1_FLASH_ORIGIN ?= 0x00100000 +GUEST1_FLASH_LENGTH ?= 0x00040000 +GUEST1_RAM_ORIGIN ?= 0x20018000 +GUEST1_RAM_LENGTH ?= 0x00018000 diff --git a/tests/target/detect_mcxn947.sh b/tests/target/detect_mcxn947.sh new file mode 100755 index 00000000..93447506 --- /dev/null +++ b/tests/target/detect_mcxn947.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Board-present check for the FRDM-MCXN947 rig: the on-board MCU-Link probe +# (NXP 1fc9:0143), its VCOM, a pyOCD with the MCXN947 pack, and SPSDK's blhost +# for the ISP life-cycle path. Exit 0 when usable; otherwise print one SKIP +# reason and exit 1. +set -u + +MCXN947_SERIAL="${MCXN947_SERIAL:-/dev/ttyACM0}" +MCXN947_PYOCD="${MCXN947_PYOCD:-pyocd}" + +if ! command -v lsusb >/dev/null 2>&1 || ! lsusb 2>/dev/null | grep -q "1fc9:0143"; then + echo "MCU-Link probe (1fc9:0143) not attached" + exit 1 +fi +if [ ! -c "$MCXN947_SERIAL" ]; then + echo "board console $MCXN947_SERIAL not present" + exit 1 +fi +if ! command -v "$MCXN947_PYOCD" >/dev/null 2>&1; then + echo "pyocd not installed" + exit 1 +fi +if ! "$MCXN947_PYOCD" pack find mcxn947 2>/dev/null | grep -q "True"; then + echo "pyocd MCXN947 pack not installed (pyocd pack install MCXN947)" + exit 1 +fi +if ! command -v blhost >/dev/null 2>&1; then + echo "spsdk (blhost) not installed" + exit 1 +fi +exit 0 diff --git a/tests/target/lib/mcxn947_wolfboot.sh b/tests/target/lib/mcxn947_wolfboot.sh new file mode 100644 index 00000000..988b2688 --- /dev/null +++ b/tests/target/lib/mcxn947_wolfboot.sh @@ -0,0 +1,71 @@ +# shellcheck shell=sh +# The pinned MCXN947 wolfBoot first stage the MCXN947 runner boots: the +# secure-application handoff branch (wolfSSL/wolfBoot#948, pinned at its +# fork ref until it merges; bump MCXN947_WOLFBOOT_REPO/REF to upstream then), +# built from the mcxn-tz-wolftrust config. Sourced; needs $here (tests/target). +MCXN947_WOLFBOOT_REPO=https://github.com/aidangarske/wolfBoot.git +MCXN947_WOLFBOOT_REF=1d47fa7b622b654a8e6a53cf53e7811c42618505 +MCXN947_WOLFBOOT_CONFIG=config/examples/mcxn-tz-wolftrust.config + +# What a cached tree must match: the ref, the config, the carried life cycle +# patch (the MCXN HAL at the pin reports a fixed SECURED life cycle; the +# patch reads the part's), and the make overrides. +mcxn947_wolfboot_stamp() { + # shellcheck disable=SC2154 # $here is set by the sourcing runner + printf '%s %s %s [%s]' "$MCXN947_WOLFBOOT_REF" "$MCXN947_WOLFBOOT_CONFIG" \ + "$(cksum "$here/wolfboot-mcxn947-lifecycle.patch" | cut -d' ' -f1)" "$*" +} + +mcxn947_wolfboot_current() { + [ -s "$1/wolfboot.bin" ] && [ -x "$1/tools/keytools/sign" ] && + [ -s "$1/wolfboot_signing_private_key.der" ] && + [ "$(cat "$1/.wt_first_stage" 2>/dev/null)" = "$2" ] +} + +# The MCUXpresso SDK the wolfBoot MCXN HAL compiles against: the environment +# (as the CI stages it), else the sibling checkout wolfBoot's config expects. +mcxn947_wolfboot_sdk() { + if [ -z "${MCUXPRESSO:-}" ] && [ -d "$HOME/NXP/mcuxpresso-sdk/mcuxsdk" ]; then + MCUXPRESSO="$HOME/NXP/mcuxpresso-sdk/mcuxsdk" + export MCUXPRESSO + fi + if [ -z "${MCUXPRESSO_CMSIS:-}" ] && [ -d "$HOME/NXP/CMSIS_5/CMSIS" ]; then + MCUXPRESSO_CMSIS="$HOME/NXP/CMSIS_5/CMSIS" + export MCUXPRESSO_CMSIS + fi + [ -n "${MCUXPRESSO:-}" ] && [ -d "$MCUXPRESSO" ] && + [ -n "${MCUXPRESSO_CMSIS:-}" ] && [ -d "$MCUXPRESSO_CMSIS" ] +} + +# mcxn947_wolfboot_build [make VAR=value...]: builds /wolfboot.bin +# at the pin unless the tree there already matches; nonzero on any failure. +mcxn947_wolfboot_build() { + mcxn947_dir="$1" + shift + mcxn947_stamp=$(mcxn947_wolfboot_stamp "$@") + if mcxn947_wolfboot_current "$mcxn947_dir" "$mcxn947_stamp"; then + return 0 + fi + mcxn947_wolfboot_sdk || { + echo "MCXN947 wolfBoot needs the MCUXpresso SDK: set MCUXPRESSO and MCUXPRESSO_CMSIS" >&2 + return 1 + } + rm -rf "$mcxn947_dir" && + git clone --no-checkout "$MCXN947_WOLFBOOT_REPO" "$mcxn947_dir" && + git -C "$mcxn947_dir" fetch --depth 1 origin "$MCXN947_WOLFBOOT_REF" && + git -C "$mcxn947_dir" checkout --detach "$MCXN947_WOLFBOOT_REF" && + git -C "$mcxn947_dir" apply "$here/wolfboot-mcxn947-lifecycle.patch" && + git -C "$mcxn947_dir" submodule update --init --single-branch --depth 1 && + cp "$mcxn947_dir/$MCXN947_WOLFBOOT_CONFIG" "$mcxn947_dir/.config" && + # keygen writes src/keystore.c, which the loader links, so the key and the + # tools that locate it from the working directory come first. wolfBoot's + # TARGET comes from its .config, never from a calling make. + ( + unset TARGET MAKEFLAGS MFLAGS + cd "$mcxn947_dir" && + make keytools && + make -j1 wolfboot_signing_private_key.der && + make -j1 "$@" wolfboot.bin + ) && + printf '%s\n' "$mcxn947_stamp" > "$mcxn947_dir/.wt_first_stage" +} diff --git a/tests/target/lib/scenario_matrix.py b/tests/target/lib/scenario_matrix.py index 992a2613..f5feb48b 100755 --- a/tests/target/lib/scenario_matrix.py +++ b/tests/target/lib/scenario_matrix.py @@ -158,6 +158,49 @@ ), "l3_exempt": {}, }, + "mcxn947": { + "label": "ci:mcxn947", + "arm": "mcxn947-m33mu", + "image": "ghcr.io/wolfssl/wolfboot-ci-m33mu:v1.25", + "smoke": ("positive", "sauneg", "crossdomain", "bothpsa", "confboot", + "devcrypto"), + "groups": ( + ("positive", "MCXN947 positive lifecycle (SAU guest windows)"), + ("sauneg", "MCXN947 cross-guest store faults and is contained"), + ("restart authneg", "MCXN947 guest restart budget and launch refusal"), + ("rollbackneg remeasureneg manifestneg spbudgetneg", + "MCXN947 secure verdict negatives"), + ("crossdomain keystoreneg", "MCXN947 SP domain isolation negatives"), + ("spfaultneg panicneg", "MCXN947 SP fault and panic recovery"), + ("fpneg", "MCXN947 FP isolation (partition FP faults, contained)"), + ("sealbootneg", "MCXN947 damaged main-stack seal refuses to boot"), + ("bothpsa bothiso", "MCXN947 both-guest PSA lifecycle and isolation"), + ("attestneg fwustage", + "MCXN947 attestation negatives and FWU staging"), + ("hsmattackneg", + "MCXN947 wolfHSM cross-namespace + NVM relay negatives"), + ("confboot", "MCXN947 FF-M IPC conformance (85/4)"), + ("devstorage devattest devattestqcbor", + "MCXN947 dev_apis storage and attestation conformance"), + ("devcrypto vaultrecover vaultrecoversec", + "MCXN947 dev_apis crypto conformance and vault recovery"), + ("periphneg", "MCXN947 NS read of the SPM's ELS refused by the SAU"), + ("deputyneg", "MCXN947 keystore-flash privileged deputy refused (L3)"), + ("hsmpinneg hsmfaultneg", + "MCXN947 wolfHSM pointer pin and tasklet fault containment (L3)"), + ("bandneg1 bandneg2 bandneg3 bandneg4 bandneg5 bandneg6", + "MCXN947 partition band-to-band isolation negatives (L3)"), + ("restartneg1 restartneg2 restartneg3", + "MCXN947 partitions restart on a reset band (L3)"), + ("periphspneg manifestneg2 manifestneg3", + "MCXN947 SPM peripheral and manifest level negatives (L3)"), + ("sealneg sealhaltneg sealpivotneg", + "MCXN947 stack seal containment and halt (L3)"), + ("mspovfneg xnneg svcneg", + "MCXN947 SPM stack, execute-never and SVC misuse (L3)"), + ), + "l3_exempt": {}, + }, } @@ -218,18 +261,29 @@ def ci_plan(event, labels, port_input): SELFTEST = ( - ("pull_request", "", "", "mimxrt700:smoke stm32h563:smoke"), - ("pull_request", "ci:rt700", "", "mimxrt700:full stm32h563:smoke"), - ("pull_request", "ci:h5", "", "mimxrt700:smoke stm32h563:full"), - ("pull_request", "ci:all", "", "mimxrt700:full stm32h563:full"), - ("pull_request", "ci:m33mu", "", "mimxrt700:full stm32h563:full"), - ("pull_request", "ci:h5 ci:rt700", "", "mimxrt700:full stm32h563:full"), - ("push", "", "", "mimxrt700:full stm32h563:full"), - ("schedule", "", "", "mimxrt700:full stm32h563:full"), - ("workflow_dispatch", "", "", "mimxrt700:full stm32h563:full"), - ("workflow_dispatch", "", "all", "mimxrt700:full stm32h563:full"), + ("pull_request", "", "", + "mcxn947:smoke mimxrt700:smoke stm32h563:smoke"), + ("pull_request", "ci:rt700", "", + "mcxn947:smoke mimxrt700:full stm32h563:smoke"), + ("pull_request", "ci:h5", "", + "mcxn947:smoke mimxrt700:smoke stm32h563:full"), + ("pull_request", "ci:mcxn947", "", + "mcxn947:full mimxrt700:smoke stm32h563:smoke"), + ("pull_request", "ci:all", "", + "mcxn947:full mimxrt700:full stm32h563:full"), + ("pull_request", "ci:m33mu", "", + "mcxn947:full mimxrt700:full stm32h563:full"), + ("pull_request", "ci:h5 ci:rt700", "", + "mcxn947:smoke mimxrt700:full stm32h563:full"), + ("push", "", "", "mcxn947:full mimxrt700:full stm32h563:full"), + ("schedule", "", "", "mcxn947:full mimxrt700:full stm32h563:full"), + ("workflow_dispatch", "", "", + "mcxn947:full mimxrt700:full stm32h563:full"), + ("workflow_dispatch", "", "all", + "mcxn947:full mimxrt700:full stm32h563:full"), ("workflow_dispatch", "", "mimxrt700", "mimxrt700:full"), ("workflow_dispatch", "", "stm32h563", "stm32h563:full"), + ("workflow_dispatch", "", "mcxn947", "mcxn947:full"), ) diff --git a/tests/target/m33mu-mcxn947.patch b/tests/target/m33mu-mcxn947.patch new file mode 100644 index 00000000..5c1c1483 --- /dev/null +++ b/tests/target/m33mu-mcxn947.patch @@ -0,0 +1,240 @@ +diff --git a/cpu/mcxn947/mcxn947_mmio.c b/cpu/mcxn947/mcxn947_mmio.c +index 831de76..875d3cf 100644 +--- a/cpu/mcxn947/mcxn947_mmio.c ++++ b/cpu/mcxn947/mcxn947_mmio.c +@@ -104,6 +104,8 @@ + #define SYSCON_PRESETCTRL0 0x100u + #define SYSCON_AHBCLKCTRL0 0x200u + #define SYSCON_AHBCLKCTRL1 0x204u ++#define SYSCON_ELS_OTP_LC_STATE 0x980u ++#define SYSCON_ELS_OTP_LC_STATE_DP 0x984u + + struct gpio_bank { + mm_u32 regs[GPIO_SIZE / 4]; +@@ -676,6 +678,10 @@ void mm_mcxn947_mmio_reset(void) + /* Initialize SYSCON reset states */ + syscon.regs[SYSCON_AHBCLKCTRL0 / 4] = 0x021FFE603u; /* + ELS clock bit */ + syscon.regs[SYSCON_PRESETCTRL0 / 4] = 0x021FFE000u; /* + ELS reset released */ ++ /* ELS_OTP_LC_STATE and its duplicate: OEM_OPEN (0x03), the life cycle a ++ * FRDM board ships in, as the ROM reflects it from OTP. */ ++ syscon.regs[SYSCON_ELS_OTP_LC_STATE / 4] = 0x03u; ++ syscon.regs[SYSCON_ELS_OTP_LC_STATE_DP / 4] = 0x03u; + + scg.regs[SCG_VERID / 4u] = 0x00000000u; + scg.regs[SCG_PARAM / 4u] = 0x000001FEu; +diff --git a/cpu/mcxn947/mcxn947_secure.c b/cpu/mcxn947/mcxn947_secure.c +index 148ef22..3f1941c 100644 +--- a/cpu/mcxn947/mcxn947_secure.c ++++ b/cpu/mcxn947/mcxn947_secure.c +@@ -26,6 +26,36 @@ + #define MCXN947_SEC_ALIAS 0x10000000u + #define MCXN947_SEC_SIZE 0x1000u + ++/* The EdgeLock (S50) command interface at its documented base: enough of the ++ * register map for a Secure caller to enable the block and run RND_REQ, which ++ * fills the DMA result buffer. The block refuses Non-secure accesses. */ ++#define MCXN947_S50_BASE 0x40054000u ++#define S50_REG_STATUS 0x000u ++#define S50_REG_CTRL 0x004u ++#define S50_REG_CMDCFG0 0x008u ++#define S50_REG_CFG 0x00Cu ++#define S50_REG_DMA_RES0 0x038u ++#define S50_REG_DMA_RES0_LEN 0x03Cu ++#define S50_REG_INT_ENABLE 0x040u ++#define S50_REG_INT_STATUS_CLR 0x044u ++#define S50_REG_INT_STATUS_SET 0x048u ++#define S50_REG_ERR_STATUS 0x04Cu ++#define S50_REG_ERR_STATUS_CLR 0x050u ++#define S50_REG_VERSION 0x054u ++#define S50_REG_PRNG_DATOUT 0x05Cu ++#define S50_REG_WORDS (0x80u / 4u) ++#define S50_STATUS_ERR (1u << 2) ++#define S50_STATUS_DRBG_ENT_LVL (3u << 8) ++#define S50_CTRL_EN (1u << 0) ++#define S50_CTRL_START (1u << 1) ++#define S50_CTRL_RESET (1u << 2) ++#define S50_CTRL_CMD_SHIFT 3u ++#define S50_CTRL_CMD_MASK 0x1Fu ++#define S50_CMD_RND_REQ 24u ++#define S50_ERR_OPN (1u << 1) ++#define S50_ERR_ALG (1u << 2) ++#define S50_RND_MAX 4096u ++ + #define SEC_REG_CMD 0x000u + #define SEC_REG_STATUS 0x004u + #define SEC_REG_ARG0 0x008u +@@ -96,13 +126,21 @@ struct mcxn947_secure_persist { + struct mcxn947_secure_slot slots[MCXN947_ELS_SLOTS]; + }; + ++struct mcxn947_s50_state { ++ mm_u32 regs[S50_REG_WORDS]; ++ mm_u32 err_status; ++ mm_bool enabled; ++}; ++ + struct mcxn947_secure_state { + struct mcxn947_secure_regs puf; + struct mcxn947_secure_regs els; + struct mcxn947_secure_regs pkc; + struct mcxn947_secure_regs otpc; + struct mcxn947_secure_regs prince; ++ struct mcxn947_s50_state s50; + struct mcxn947_secure_persist persist; ++ struct mm_memmap *map; + mm_u8 *flash; + mm_u32 flash_size; + mm_u32 lifecycle; +@@ -686,6 +724,125 @@ static mm_bool secure_region_write(void *opaque, mm_u32 offset, mm_u32 size_byte + return MM_FALSE; + } + ++/* RND_REQ: the DMA result buffer is filled a word at a time through the ++ * memory map, as the ELS DMA master would write it. */ ++static void s50_rnd_req(struct mcxn947_s50_state *s) ++{ ++ mm_u8 block[S50_RND_MAX]; ++ mm_u32 addr = s->regs[S50_REG_DMA_RES0 / 4u]; ++ mm_u32 len = s->regs[S50_REG_DMA_RES0_LEN / 4u]; ++ mm_u32 i; ++ if (g_sec.map == 0 || len == 0u || len > S50_RND_MAX || ++ (len % 4u) != 0u || (addr % 4u) != 0u) { ++ s->err_status |= S50_ERR_OPN; ++ return; ++ } ++ mm_mcxn947_secure_rng_fill(block, len); ++ for (i = 0u; i < len; i += 4u) { ++ if (!mm_memmap_write(g_sec.map, MM_SECURE, addr + i, 4u, ++ read32le(block + i))) { ++ s->err_status |= S50_ERR_OPN; ++ return; ++ } ++ } ++} ++ ++static void s50_ctrl_write(struct mcxn947_s50_state *s, mm_u32 value) ++{ ++ mm_u32 cmd = (value >> S50_CTRL_CMD_SHIFT) & S50_CTRL_CMD_MASK; ++ if ((value & S50_CTRL_RESET) != 0u) { ++ s->err_status = 0u; ++ s->enabled = MM_FALSE; ++ return; ++ } ++ s->enabled = ((value & S50_CTRL_EN) != 0u) ? MM_TRUE : MM_FALSE; ++ if ((value & S50_CTRL_START) == 0u) { ++ return; ++ } ++ if (!s->enabled || !mm_mcxn947_secure_els_clock_ready()) { ++ s->err_status |= S50_ERR_OPN; ++ return; ++ } ++ if (cmd == S50_CMD_RND_REQ) { ++ s50_rnd_req(s); ++ } else { ++ s->err_status |= S50_ERR_ALG; ++ } ++} ++ ++static mm_bool s50_read(void *opaque, mm_u32 offset, mm_u32 size_bytes, ++ mm_u32 *value_out) ++{ ++ struct mcxn947_s50_state *s = (struct mcxn947_s50_state *)opaque; ++ mm_u8 word[4]; ++ if (!secure_access_ok()) return MM_FALSE; ++ if (s == 0 || value_out == 0 || size_bytes != 4u) return MM_FALSE; ++ if (offset >= S50_REG_WORDS * 4u) return MM_FALSE; ++ switch (offset) { ++ case S50_REG_STATUS: ++ *value_out = S50_STATUS_DRBG_ENT_LVL | ++ ((s->err_status != 0u) ? S50_STATUS_ERR : 0u); ++ return MM_TRUE; ++ case S50_REG_ERR_STATUS: ++ *value_out = s->err_status; ++ return MM_TRUE; ++ case S50_REG_VERSION: ++ *value_out = 0x00050000u; ++ return MM_TRUE; ++ case S50_REG_PRNG_DATOUT: ++ mm_mcxn947_secure_rng_fill(word, sizeof(word)); ++ *value_out = read32le(word); ++ return MM_TRUE; ++ default: ++ *value_out = s->regs[offset / 4u]; ++ return MM_TRUE; ++ } ++} ++ ++static mm_bool s50_write(void *opaque, mm_u32 offset, mm_u32 size_bytes, ++ mm_u32 value) ++{ ++ struct mcxn947_s50_state *s = (struct mcxn947_s50_state *)opaque; ++ if (!secure_access_ok()) return MM_FALSE; ++ if (s == 0 || size_bytes != 4u) return MM_FALSE; ++ if (offset >= S50_REG_WORDS * 4u) return MM_FALSE; ++ switch (offset) { ++ case S50_REG_STATUS: ++ case S50_REG_ERR_STATUS: ++ case S50_REG_VERSION: ++ case S50_REG_PRNG_DATOUT: ++ return MM_FALSE; ++ case S50_REG_CTRL: ++ s->regs[offset / 4u] = value & ~S50_CTRL_START; ++ s50_ctrl_write(s, value); ++ return MM_TRUE; ++ case S50_REG_ERR_STATUS_CLR: ++ if ((value & 1u) != 0u) s->err_status = 0u; ++ return MM_TRUE; ++ case S50_REG_INT_STATUS_CLR: ++ case S50_REG_INT_STATUS_SET: ++ return MM_TRUE; ++ default: ++ s->regs[offset / 4u] = value; ++ return MM_TRUE; ++ } ++} ++ ++static mm_bool secure_register_s50(struct mmio_bus *bus) ++{ ++ struct mmio_region reg; ++ memset(®, 0, sizeof(reg)); ++ reg.base = MCXN947_S50_BASE; ++ reg.size = MCXN947_SEC_SIZE; ++ reg.opaque = &g_sec.s50; ++ reg.read = s50_read; ++ reg.write = s50_write; ++ if (!mmio_bus_register_region(bus, ®)) return MM_FALSE; ++ reg.base = MCXN947_S50_BASE + MCXN947_SEC_ALIAS; ++ if (!mmio_bus_register_region(bus, ®)) return MM_FALSE; ++ return MM_TRUE; ++} ++ + static mm_bool secure_register_pair(struct mmio_bus *bus, mm_u32 base, + struct mcxn947_secure_regs *regs) + { +@@ -710,6 +867,7 @@ void mm_mcxn947_secure_reset(void) + secure_reset_regs(&g_sec.els); + secure_reset_regs(&g_sec.pkc); + secure_reset_regs(&g_sec.prince); ++ memset(&g_sec.s50, 0, sizeof(g_sec.s50)); + g_sec.puf.status |= SEC_STATUS_DONE; + g_sec.otpc.status |= SEC_STATUS_DONE; + g_sec.els.status |= SEC_STATUS_DONE; +@@ -730,6 +888,7 @@ mm_bool mm_mcxn947_secure_register_mmio(struct mmio_bus *bus) + if (!secure_register_pair(bus, MCXN947_ELS_BASE, &g_sec.els)) return MM_FALSE; + if (!secure_register_pair(bus, MCXN947_PKC_BASE, &g_sec.pkc)) return MM_FALSE; + if (!secure_register_pair(bus, MCXN947_PRINCE_BASE, &g_sec.prince)) return MM_FALSE; ++ if (!secure_register_s50(bus)) return MM_FALSE; + return MM_TRUE; + } + +@@ -739,9 +898,9 @@ void mm_mcxn947_secure_flash_bind(struct mm_memmap *map, + const struct mm_flash_persist *persist, + mm_u32 flags) + { +- (void)map; + (void)persist; + (void)flags; ++ g_sec.map = map; + g_sec.flash = flash; + g_sec.flash_size = flash_size; + secure_refresh_identity(&g_sec); diff --git a/tests/target/run_mcxn947_m33mu.sh b/tests/target/run_mcxn947_m33mu.sh new file mode 100755 index 00000000..a1b1680e --- /dev/null +++ b/tests/target/run_mcxn947_m33mu.sh @@ -0,0 +1,620 @@ +#!/usr/bin/env bash +# wolfTrust on the NXP MCXN947 under the M33MU emulator (--cpu mcxn947): the +# same chain the FRDM-MCXN947 boots (wolfBoot -> wolfTrust secure -> two +# baremetal guests), the MIMXRT700 scenario names, asserted from the emulator +# log. This is emulator evidence, never silicon evidence. +# +# positive wolfBoot verifies wolfTrust; both guests launch, reach the SPM +# through the SG veneers and finish with no fault anywhere. +# sauneg positive plus the CPU isolation negative: guest0 stores into +# guest1's RAM window, which the per-dispatch SAU window keeps +# Secure, so the store faults. The monitor contains it, relaunches +# guest0 through its restart budget, quarantines it, and guest1 +# keeps running. +# +# rollbackneg, remeasureneg, manifestneg, spbudgetneg, and sealbootneg are the +# shared Secure-verdict scenarios from lib/scenario.sh: each ends on the +# verdict breakpoint its probe emits, asserted by the port-independent table. +# crossdomain and keystoreneg fault the storage SP on an out-of-domain read; +# spfaultneg and panicneg fault an SP on its first entry and prove the SPM +# restarts it in place while both guests finish; fpneg proves only that the +# relay's FP instruction takes a NOCP UsageFault. restart spends guest0's +# restart budget on a launch-time fault; authneg refuses a tampered guest0 at +# launch. guest1 runs on through all of them. +# +# bothpsa, bothiso, attestneg, hsmattackneg, and fwustage run the portable +# PSA test guest (tests/firmware/psa-guest) in both windows; hsmattackneg +# drives the raw wolfHSM client wire, so it exists only under WT_ENGINE=hsm. +# +# confboot, devstorage, devcrypto, devattest, devattestqcbor, vaultrecover, and +# vaultrecoversec host Arm's unmodified psa-arch-tests val NSPE in the PSA guest +# (guest0) against the conformance Secure image; guest1 is the small bare-metal +# guest so the boot cycles of the IPC suite's panic tests stay cheap. +# +# Environment (all optional): +# M33MU prebuilt emulator carrying lib's MCXN947 model patch +# (tests/target/m33mu-mcxn947.patch); otherwise +# M33MU_REF plus that patch is built under /tmp +# MCXN947_WOLFBOOT_DIR wolfBoot tree holding wolfboot.bin, tools/keytools/ +# sign and wolfboot_signing_private_key.der, built by +# lib/mcxn947_wolfboot.sh from the mcxn-tz-wolftrust +# config with the carried life cycle patch; otherwise +# the pinned ref is built under /tmp (the MCUXpresso +# SDK must be reachable: MCUXPRESSO and +# MCUXPRESSO_CMSIS, or ~/NXP as wolfBoot expects) +# MCXN947_M33MU_TIMEOUT emulator wall-clock budget in seconds (default 60, +# 180 for the PSA guest scenarios) +set -eu +# make test-target hands TARGET and MAKEFLAGS to every child make; wolfBoot's +# own TARGET must come from its config, so drop both before any build. +unset TARGET MAKEFLAGS MFLAGS + +scenario="${1:-}" +# The scenario table in lib/scenario_matrix.py is the one list of names. +known="$(python3 "$(dirname "$0")/lib/scenario_matrix.py" --port mcxn947 \ + --tier full --flat)" +case " $known " in + *" $scenario "*) ;; + *) echo "usage: $0 $(echo "$known" | tr ' ' '|')" >&2; exit 2 ;; +esac +case "$scenario" in + hsmattackneg|hsmpinneg|hsmfaultneg) + if [ "${WT_ENGINE:-native}" != "hsm" ]; then + echo "$scenario probes the wolfHSM engine; run it with WT_ENGINE=hsm" >&2 + exit 2 + fi ;; +esac + +here="$(cd "$(dirname "$0")" && pwd)" +repo="$(cd "$here/../.." && pwd)" +case "$scenario" in + bothpsa|bothiso|attestneg|hsmattackneg|fwustage|deputyneg|hsmpinneg|hsmfaultneg|bandneg[1-6]|restartneg[1-3]|periphspneg|sealneg|sealpivotneg|svcneg) + guest_dir="tests/firmware/psa-guest" + guest1_dir="$guest_dir" + timeout_s="${MCXN947_M33MU_TIMEOUT:-180}" ;; + confboot|devstorage|devcrypto|devattest|devattestqcbor|vaultrecover|vaultrecoversec) + guest_dir="tests/firmware/psa-guest" + guest1_dir="tests/firmware/mcxn947-baremetal" + timeout_s="${MCXN947_M33MU_TIMEOUT:-900}" ;; + *) + guest_dir="tests/firmware/mcxn947-baremetal" + guest1_dir="$guest_dir" + timeout_s="${MCXN947_M33MU_TIMEOUT:-60}" ;; +esac +guest_build="$repo/$guest_dir/build" +guest1_build="$repo/$guest1_dir/build" + +# shellcheck source=lib/mcxn947_wolfboot.sh disable=SC1091 +. "$here/lib/mcxn947_wolfboot.sh" +wolfboot_dir="${MCXN947_WOLFBOOT_DIR:-/tmp/wolfboot_mcxn947}" +log="$repo/build/mcxn947_m33mu_$scenario.log" + +# The upstream M33MU commit with the MCXN947 RAM attribution fix; the carried +# patch adds the EdgeLock RND_REQ register model the SPM seeds its DRBG from. +M33MU_REF=87f693a3cbc4c1315ed3676cef962c092eb2f7be + +# The guests' manifest restart budget (port/mcxn947/partitions.c): sauneg +# relaunches guest0 this many times before quarantining it. +restart_limit=3 +# Where guest0's probe stores: inside guest1's RAM window (GUEST_PROBE_ADDR in +# tests/firmware/mcxn947-baremetal/Makefile). +probe_addr=0x20028000 +# Where guest0's restart probe reads: Secure RAM (GUEST_FAULT_ADDR there). +fault_addr=0x30031000 + +# shellcheck source=lib/scenario.sh disable=SC1091 +. "$here/lib/scenario.sh" +l3_layout_load "$repo" mcxn947 + +# Port markers the shared verdict assertions match against. +# shellcheck disable=SC2034 # matched inside lib/scenario.sh +GUEST_STARTED_RE='wolfTrust MCXN947 guest[01]: start' +# shellcheck disable=SC2034 +GUEST_DONE_RE='wolfTrust MCXN947 guest[01]: FF-M connect ok, done' + +# --- The pinned upstream M33MU plus the carried MCXN947 model patch. --- +m33mu_stamp="$M33MU_REF $(cksum "$here/m33mu-mcxn947.patch" | cut -d' ' -f1)" +if [ -n "${M33MU:-}" ] && [ -x "$M33MU" ]; then + log "Using prebuilt M33MU: $M33MU" +elif [ -x /tmp/m33mu_mcxn947_src/build/m33mu ] && + [ "$(cat /tmp/m33mu_mcxn947_src/.wt_m33mu 2>/dev/null)" = "$m33mu_stamp" ]; then + M33MU=/tmp/m33mu_mcxn947_src/build/m33mu + log "Reusing M33MU from a prior scenario: $M33MU" +else + stage "build M33MU $M33MU_REF" + rm -rf /tmp/m33mu_mcxn947_src + git clone --no-checkout https://github.com/danielinux/m33mu.git /tmp/m33mu_mcxn947_src + git -C /tmp/m33mu_mcxn947_src fetch --depth 1 origin "$M33MU_REF" + git -C /tmp/m33mu_mcxn947_src checkout --detach "$M33MU_REF" + git -C /tmp/m33mu_mcxn947_src apply "$here/m33mu-mcxn947.patch" + cmake -S /tmp/m33mu_mcxn947_src -B /tmp/m33mu_mcxn947_src/build \ + -DM33MU_ENABLE_WOLFSSL=OFF -DM33MU_BUILD_TESTS=OFF \ + -DM33MU_ENABLE_RUST_PLUGINS=OFF + cmake --build /tmp/m33mu_mcxn947_src/build --target m33mu -j"$(nproc)" + printf '%s\n' "$m33mu_stamp" > /tmp/m33mu_mcxn947_src/.wt_m33mu + M33MU=/tmp/m33mu_mcxn947_src/build/m33mu +fi + +# --- Secure-app wolfBoot first stage from the pinned mcxn-tz-wolftrust +# config: wolfBoot at the start of the flash, BOOT at 0x40000, the +# handoff record at 0x30030000, every image offset physical. --- +if [ -n "${MCXN947_WOLFBOOT_DIR:-}" ]; then + mcxn947_wolfboot_current "$wolfboot_dir" "$(mcxn947_wolfboot_stamp)" || \ + fail "wolfboot.bin in MCXN947_WOLFBOOT_DIR=$wolfboot_dir was not built from $MCXN947_WOLFBOOT_REF with $MCXN947_WOLFBOOT_CONFIG and the carried life cycle patch; rebuild it there (tests/target/lib/mcxn947_wolfboot.sh) or unset it" +else + stage "wolfBoot $MCXN947_WOLFBOOT_REF (mcxn-tz-wolftrust)" + mcxn947_wolfboot_build "$wolfboot_dir" || fail "wolfBoot build failed" +fi + +# --- wolfTrust and both guests, measurements pinned, signed for the boot +# partition. --- +cd "$repo" +git submodule update --init --single-branch +# MCXN wolfBoot uses a 1024-byte image header, so wolfTrust links at the boot +# base + 0x400 and is signed with a matching header. +export WT_SECURE_IMAGE_HEADER_SIZE=0x400 +rm -rf build +mkdir -p build + +secure_flags="$(scenario_secure_flags "$scenario")" +# Exported, not passed once: the guest build below re-enters the secure +# Makefile for the CMSE implib, and a build-mode mismatch there would relink +# the image and drop the wolftrust.bin this runner goes on to patch and sign. +if [ -n "$secure_flags" ]; then + # shellcheck disable=SC2086,SC2163 + export $secure_flags +fi +stage "build wolfTrust secure image + CMSE import library ${secure_flags:-(production)}" +make -s TARGET=mcxn947 secure-image TOOLPREFIX=arm-none-eabi- + +guest_flags="" +case "$scenario" in + sauneg) guest_flags="WT_SAU_PROBE=1" ;; + periphneg) guest_flags="WT_PERIPH_NEG_PROBE=1" ;; + # The secure probe fires inside a partition; guest0 ends the run on its + # breakpoint once its lifecycle is done, as the H5 level 3 runs do. + deputyneg|hsmpinneg|hsmfaultneg|bandneg[1-6]|restartneg[1-3]|periphspneg|sealneg|sealpivotneg|svcneg) + guest_flags="WT_M33MU_EXPECT_BKPT=1" + # guest0's own tasklet faults, so the peer ends the run. + if [ "$scenario" = "hsmfaultneg" ]; then + guest_flags="$guest_flags WT_M33MU_BKPT_GUEST=1" + fi ;; + restart) guest_flags="WT_GUEST_FAULT_PROBE=1" ;; + attestneg) guest_flags="WT_ATTEST_NEG_PROBE=1" ;; + hsmattackneg) guest_flags="WT_HSM_ATTACK_PROBE=1" ;; + # 0x21000 bytes of body reach sector 16 of the 32-sector update partition. + fwustage) guest_flags="WT_FWU_PROBE=1 WT_FWU_PROBE_STREAM_BYTES=0x21000u WT_FWU_PROBE_SEQUENTIAL=1" ;; + confboot) guest_flags="WT_RUN_CONFORMANCE=1 WT_M33MU_EXPECT_BKPT=1" ;; + devstorage) guest_flags="WT_RUN_CONFORMANCE=1 WT_CONF_SUITE=storage WT_M33MU_EXPECT_BKPT=1" ;; + devcrypto|vaultrecover|vaultrecoversec) + guest_flags="WT_RUN_CONFORMANCE=1 WT_CONF_SUITE=crypto WT_M33MU_EXPECT_BKPT=1" ;; + devattest) guest_flags="WT_RUN_CONFORMANCE=1 WT_CONF_SUITE=attestation WT_M33MU_EXPECT_BKPT=1" ;; + devattestqcbor) + tests/upstream/fetch_qcbor.sh >/dev/null + guest_flags="WT_RUN_CONFORMANCE=1 WT_CONF_SUITE=attestation WT_ATTEST_CBOR=qcbor WT_M33MU_EXPECT_BKPT=1" ;; +esac +stage "build the Non-secure guests from $guest_dir ${guest_flags:-(no probes)}" +make -s -C "$guest_dir" clean +# The emulator boots in OEM_OPEN, the ASSEMBLY_AND_TEST life cycle the +# attestation token reports; the guest's verify pins it. +# shellcheck disable=SC2086 +make -s -C "$guest_dir" TARGET=mcxn947 WT_EXPECTED_LIFECYCLE=0x1000u $guest_flags +if [ "$guest1_dir" != "$guest_dir" ]; then + make -s -C "$guest1_dir" clean + make -s -C "$guest1_dir" TARGET=mcxn947 +fi + +stage "pin both guest measurements, then wolfBoot-sign wolfTrust" +python3 tools/measure/patch_guest_digests.py build/wolftrust.bin \ + "0:1:$guest_build/guest0.bin" "1:1:$guest1_build/guest1.bin" +IMAGE_HEADER_SIZE=1024 WOLFBOOT_PARTITION_SIZE=0x40000 WOLFBOOT_SECTOR_SIZE=0x2000 \ + "$wolfboot_dir/tools/keytools/sign" --ecc256 --sha256 build/wolftrust.bin \ + "$wolfboot_dir/wolfboot_signing_private_key.der" 1 +[ -s build/wolftrust_v1_signed.bin ] || fail "signing produced no image" +# The harness, not the guest, holds wolfBoot's measurement of the signed image +# and matches the attestation token's reported value against it. +expected_measurement="$(python3 tests/scripts/read_wolfboot_measurement.py \ + build/wolftrust_v1_signed.bin)" + +if [ "$scenario" = "authneg" ]; then + # One byte of guest0 flips after its digest was pinned: launch verification + # must refuse guest0 while guest1 and the platform keep running. + python3 - "$guest_build/guest0.bin" <<'PYEOF' +import sys +with open(sys.argv[1], "r+b") as f: + f.seek(0x40) + byte = f.read(1) + f.seek(0x40) + f.write(bytes([byte[0] ^ 0x01])) +PYEOF +fi + +# The guests idle forever once done, so a console boot ends on the wall-clock +# budget (M33MU status 127) and a --quit-on-faults boot at the fault (0). +# Flash file offsets are physical: wolfBoot at 0, BOOT at 0x40000, the guest +# windows at 0x80000 and 0x100000. +boot_chain() { + local want="$1" + local out="$2" + local status + shift 2 + set +e + "$M33MU" --cpu mcxn947 "$wolfboot_dir/wolfboot.bin" \ + build/wolftrust_v1_signed.bin:0x40000 \ + "$guest_build/guest0.bin:0x80000" \ + "$guest1_build/guest1.bin:0x100000" \ + --timeout "$timeout_s" "$@" > "$out" 2>&1 + status=$? + set -e + check "$(printf '%s' "$status" | grep -qE "^($want)$"; echo $?)" \ + "M33MU exited with status $want (got $status)" +} + +end="$(scenario_end "$scenario")" +case "$scenario" in + confboot|devstorage|devcrypto|devattest|devattestqcbor|vaultrecover|vaultrecoversec) + # The val guest ends on its own breakpoint; the suite's panic tests reset + # the whole chain mid-run and val resumes off its flash boot flag. + end="bkpt:0x7f" ;; + fpneg|xnneg|mspovfneg) + # M33MU ends these runs at the fault (NOCP, the fetch denial, STKOF). + end="fault" ;; + deputyneg|hsmpinneg|hsmfaultneg|bandneg[1-6]|restartneg[1-3]|periphspneg|sealneg|sealpivotneg|svcneg) + end="bkpt:0x7f" ;; +esac +stage "boot the chain under M33MU (--cpu mcxn947, ${timeout_s}s budget)" +case "$end" in + bkpt:*) boot_chain 0 "$log" --uart-stdout --expect-bkpt "${end#bkpt:}" ;; + fault) boot_chain "0|1" "$log" --uart-stdout --quit-on-faults ;; + *) boot_chain 127 "$log" --uart-stdout ;; +esac + +# wolfBoot's MCXN HAL prints nothing of its own; its generic loader output +# shows the measured handoff path ran. +expect "wolfBoot verified the wolfTrust image signature" "Verifying signature...done" +if [ "$end" = "idle" ]; then + refute_re "wolfTrust never panicked (bkpt 0x7e)" '\[BKPT\] imm=0x7e' + refute_re "the monitor never lost every guest (bkpt 0x7d)" '\[BKPT\] imm=0x7d' + refute_re "no HardFault escalation" '\[HARDFLT\]' + refute_re "no guest reported a failed FF-M handshake" 'guest[01]: FAIL' + refute_re "no PSA guest lifecycle step failed" \ + 'guest[01]: .*(FAILED|failed|NOT re|SUCCEEDED|accepted|unavailable|wrong data|not refused|not rejected)' + expect "the run ended on the wall-clock budget, not a trap" "wall-clock limit" +elif [ "${end}" = "bkpt:0x7f" ]; then + expect "the val guest ran to its clean exit breakpoint" "[EXPECT BKPT] Success" +elif [ "$end" = "fault" ]; then + expect "the run stopped at the first delivered fault" "Execution stopped" +else + expect "the emulator stopped on the expected verdict breakpoint" \ + "[EXPECT BKPT] Success" + scenario_assert_verdict "$scenario" +fi + +# The Arm suite prints its report once at the end; guest1's lines can splice +# it, so the totals are read from the log with guest1 stripped and unwrapped. +conf_totals() { + local flat + flat="$(sed 's/guest1:.*$//' "$log" | tr -d '\r\n')" + conf_passed=$(printf '%s' "$flat" | grep -oE 'TOTAL PASSED[[:space:]]*:[[:space:]]*[0-9]+' | grep -oE '[0-9]+' | tail -1 || true) + conf_skipped=$(printf '%s' "$flat" | grep -oE 'TOTAL SKIPPED[[:space:]]*:[[:space:]]*[0-9]+' | grep -oE '[0-9]+' | tail -1 || true) + conf_failed=$(printf '%s' "$flat" | grep -oE 'TOTAL FAILED[[:space:]]*:[[:space:]]*[0-9]+' | grep -oE '[0-9]+' | tail -1 || true) + : "${conf_passed:=-1}"; : "${conf_skipped:=-1}"; : "${conf_failed:=-1}" +} + +# A guest fault relaunches that guest, so exact launch counts also prove that +# nothing faulted where nothing should have. +case "$scenario" in + positive|crossdomain|keystoreneg|spfaultneg|panicneg) + for guest in guest0 guest1; do + expect_n "$guest launched exactly once (no fault, no relaunch)" 1 \ + "wolfTrust MCXN947 $guest: start" + expect_n "$guest reached the SPM through the SG veneers and finished" 1 \ + "wolfTrust MCXN947 $guest: FF-M connect ok, done" + done + # The deliberate SP probe faults are Secure faults the guests above prove + # the system rode out. M33MU's own fault dumps go to a buffered stdout + # that its wall-clock exit drops, so each probe is shown by a second boot + # that stops at the first delivered fault. + if [ "$scenario" = "positive" ] || [ "$scenario" = "spfaultneg" ]; then + expect_n "both guests reached the storage service" 2 ": storage connect ok" + elif [ "$scenario" = "panicneg" ]; then + # The panic unblocks the client it was serving with an error; the + # restarted SP serves the other guest. + expect_n "the restarted storage SP served the guest after the panic" 1 \ + ": storage connect ok" + fi + if [ "$scenario" = "spfaultneg" ] || [ "$scenario" = "panicneg" ]; then + # The relay (spfaultneg) runs an undefined instruction on its first + # entry; the storage SP (panicneg) closes an error handle, which the SPM + # must panic it for by resuming it on one. + stage "boot again, stopping at the first delivered fault" + log="$repo/build/mcxn947_m33mu_${scenario}_trace.log" + boot_chain 0 "$log" --quit-on-faults + expect "the traced run stopped at a delivered fault" "Execution stopped" + expect_n_re "the probed SP took a Secure-Thread UsageFault" 1 \ + '\[USGFLT\] enter sec=1 mode=0' + expect "the fault was the undefined instruction" "[USGFLT] CFSR=0x00010000" + elif [ "$scenario" != "positive" ]; then + # The storage SP's probe faults on its first wake, so the guests' + # storage connect never succeeds while their own lifecycle survives. + refute_re "the probed storage SP never served a guest connect" \ + ": storage connect ok" + case "$scenario" in + crossdomain) neg_addr="$L3_SPM_RAM" ;; + keystoreneg) neg_addr="$L3_VAULT_BAND" ;; + esac + stage "boot again with the protection-unit trace, stopping at the first fault" + log="$repo/build/mcxn947_m33mu_${scenario}_trace.log" + M33MU_PROT_TRACE=1 boot_chain 0 "$log" --quit-on-faults + expect "the traced run stopped at a delivered fault" "Execution stopped" + expect_re "the fault was the SP's out-of-domain read at the band address" \ + "\[MEMFAULT_CAUSE\] sec=S type=READ addr=$neg_addr reason=mpu-ap" + fi + ;; + deputyneg|hsmpinneg|hsmfaultneg|bandneg[1-6]|restartneg[1-3]|periphspneg|sealneg|sealpivotneg|svcneg) + expect_n "guest0 launched exactly once" 1 "guest0: alive" + if [ "$scenario" = "hsmfaultneg" ]; then + expect "guest1 ran its whole PSA lifecycle to the end breakpoint" \ + "guest1: done" + else + expect "guest0 ran its whole PSA lifecycle to the end breakpoint" \ + "guest0: done" + expect "guest1 ran beside the partition under test" "guest1: alive" + fi + case "$scenario" in + deputyneg) + l3_assert_probe_linked build/wolftrust.elf \ + wt_platform_deputy_flash_probe ;; + hsmpinneg) + l3_assert_probe_linked build/wolftrust.elf wt_platform_hsm_pin_probe ;; + *) + scenario_assert_l3 "$scenario" ;; + esac + case "$scenario" in + deputyneg|hsmpinneg|bandneg[1-6]|restartneg[1-3]) + # The probed partition is restarted (or never faults) and serves on. + if [ "$scenario" = "deputyneg" ] || [ "$scenario" = "hsmpinneg" ]; then + refute_re "no fault markers in the boot log" \ + '^(\[MEMFAULT\]|\[USGFLT\]|\[HARDFLT\]|HardFault|SecureFault)' + fi + expect "mediated crypto dispatch verified" \ + "guest0: wolfTrust FF-M mediated crypto dispatch verified" + expect "ITS set/get verified" "guest0: wolfTrust ITS set/get verified" + expect "PS sealed set/get verified" \ + "guest0: wolfTrust PS sealed set/get verified" + expect "key-ops sign/verify verified" \ + "guest0: wolfTrust key-ops sign/verify verified" + expect "attestation COSE_Sign1 verified" \ + "guest0: wolfTrust attestation: COSE_Sign1 verified" + ;; + hsmfaultneg) + expect "the other guest kept being served" \ + "guest1: psa_hash_compute(SHA-256) KAT verified" ;; + svcneg) + # The stateless ITS client reports the unblocked call as an error. + expect_re "the pinned ITS client was unblocked with an error" \ + 'guest[01]: psa_its_set failed st=-1(32|45)' + expect "the restarted ITS partition served the other guest" \ + "wolfTrust ITS set/get verified" + expect "sealed storage path unaffected" \ + "guest0: wolfTrust PS sealed set/get verified" ;; + periphspneg) + refute_re "the probed storage SP never served ITS" \ + 'guest[01]: wolfTrust ITS set/get verified' ;; + esac + ;; + xnneg|mspovfneg) + scenario_assert_verdict "$scenario" + ;; + fpneg) + # The relay's FP instruction must hit a disabled coprocessor: NOCP, not the + # fallback undefined-instruction fault. Restart and guest survival are the + # silicon leg's to show (docs/Testing.md). + expect_n_re "the relay took a Secure-Thread UsageFault" 1 \ + '\[USGFLT\] enter sec=1 mode=0' + expect "the fault was the NOCP trap (Secure FP disabled)" \ + "[USGFLT] CFSR=0x00080000" + refute_re "no HardFault escalation" '\[HARDFLT\]' + ;; + restart) + faults=$((restart_limit + 1)) + expect_n "guest0 relaunched through its restart budget, then quarantined" \ + "$faults" "wolfTrust MCXN947 guest0: start" + refute_re "guest0 never got past its launch-time fault" \ + "wolfTrust MCXN947 guest0: FF-M connect ok" + expect_n "guest1 launched once, untouched by guest0's faults" 1 \ + "wolfTrust MCXN947 guest1: start" + expect_n "guest1 completed the FF-M handshake" 1 \ + "wolfTrust MCXN947 guest1: FF-M connect ok, done" + stage "boot again with the protection-unit trace, stopping at the first fault" + log="$repo/build/mcxn947_m33mu_${scenario}_trace.log" + M33MU_PROT_TRACE=1 boot_chain 0 "$log" --quit-on-faults + expect "the traced run stopped at a delivered fault" "Execution stopped" + refused="\[MEMFAULT_CAUSE\] sec=NS type=READ addr=$fault_addr reason=secure-attr" + expect_n_re "the fault was guest0's read of Secure RAM, refused as Secure" \ + 1 "$refused" + sau_refusals="$(grep -A1 -E -- "$refused" "$log" | grep -c "src=SAU" || true)" + check "$([ "$sau_refusals" -eq 1 ]; echo $?)" \ + "the refusal came from the SAU" + ;; + authneg) + refute_re "the tampered guest0 never entered its domain" \ + "wolfTrust MCXN947 guest0: start" + expect_n "guest1 launched once after guest0's refusal" 1 \ + "wolfTrust MCXN947 guest1: start" + expect_n "guest1 completed the FF-M handshake" 1 \ + "wolfTrust MCXN947 guest1: FF-M connect ok, done" + expect_n "guest1 reached the storage service" 1 \ + "wolfTrust MCXN947 guest1: storage connect ok" + ;; + bothpsa|bothiso|attestneg|hsmattackneg|fwustage) + for guest in guest0 guest1; do + expect_n "$guest launched exactly once (no fault, no relaunch)" 1 \ + "$guest: alive" + expect_n "$guest ran its whole PSA lifecycle" 1 "$guest: done" + done + case "$scenario" in + bothpsa) + # The same PSA client behaviour from both guests in one boot. + for guest in guest0 guest1; do + expect "$guest: mediated SERVICE_CRYPTO SHA-256 KAT" \ + "$guest: wolfTrust FF-M mediated crypto dispatch verified" + expect "$guest: PSA psa_generate_random" \ + "$guest: psa_generate_random st=0" + expect "$guest: PSA psa_hash_compute(SHA-256) KAT" \ + "$guest: psa_hash_compute(SHA-256) KAT verified" + expect "$guest: PSA AES-CTR through a volatile key" \ + "$guest: psa_cipher_encrypt(AES-CTR) st=0" + expect "$guest: ITS set/get through SERVICE_ITS" \ + "$guest: wolfTrust ITS set/get verified" + expect "$guest: PS sealed set/get through SERVICE_PS" \ + "$guest: wolfTrust PS sealed set/get verified" + expect "$guest: key-ops sign/verify through the mediated path" \ + "$guest: wolfTrust key-ops sign/verify verified" + expect "$guest: cross-key verify refused" \ + "$guest: wolfTrust key negatives verified" + expect "$guest: initial attestation token issued" \ + "$guest: psa_initial_attestation st=0" + expect "$guest: COSE_Sign1 token verified against the IAK" \ + "$guest: wolfTrust attestation: COSE_Sign1 verified" + expect "$guest: token measurement equals wolfBoot's measurement of the signed image" \ + "$guest: wolfTrust attestation: token measurement=$expected_measurement" + done + ;; + bothiso) + # The SPM rejects the same abuse from both guests, and each guest + # still gets served afterwards. + for guest in guest0 guest1; do + expect "$guest: forged-handle call rejected" \ + "$guest: wolfTrust FF-M forged-handle call rejected" + expect "$guest: oversized-vector call rejected" \ + "$guest: wolfTrust FF-M oversized-vector call rejected" + expect "$guest: cross-guest vector refused (caller-banded memcheck)" \ + "$guest: wolfTrust FF-M cross-guest vector rejected" + expect "$guest: unknown-SID connect refused" \ + "$guest: wolfTrust FF-M unknown-SID connect refused" + expect "$guest: still served after the negatives" \ + "$guest: psa_hash_compute(SHA-256) KAT verified" + done + ;; + attestneg) + expect "attestation baseline token verified" \ + "guest0: wolfTrust attestation: COSE_Sign1 verified" + expect "oversized challenge rejected st=-135" \ + "guest0: attestneg oversized challenge rejected st=-135" + expect "zero token buffer rejected st=-135" \ + "guest0: attestneg zero token buffer rejected st=-135" + expect "tampered token fails the guest verify" \ + "guest0: attestneg tampered token rejected" + expect "misattributed lifecycle fails the guest verify" \ + "guest0: attestneg lifecycle mismatch rejected" + expect "attestation negatives verified" \ + "guest0: wolfTrust attestation negatives verified" + ;; + hsmattackneg) + expect "forged COMM_INIT attempted" \ + "guest0: hsmattackneg forged COMM_INIT" + expect "IAK sign refused" "guest0: hsmattackneg IAK sign refused" + expect "rollback NVM group refused" \ + "guest0: hsmattackneg rollback NVM group refused" + expect "own-namespace crypto still works" \ + "guest0: hsmattackneg own-namespace crypto still works" + ;; + fwustage) + expect "write before start refused on target" \ + "guest0: wolfTrust FWU write-before-start refused" + expect "candidate body streamed across the update partition's data sectors" \ + "guest0: wolfTrust FWU streamed candidate body across the update partition" + expect "candidate staged to the update partition and armed" \ + "guest0: wolfTrust FWU staged signed-header candidate to update partition, armed, verified" + expect "reject disarms and clean restores READY" \ + "guest0: wolfTrust FWU reject disarmed and clean restored READY" + expect "a write that skips ahead is refused and the session cleans" \ + "guest0: wolfTrust FWU out-of-order write refused and cleaned" + expect "unrelated storage partitions unaffected" \ + "guest0: wolfTrust ITS set/get verified" + ;; + esac + ;; + confboot|devstorage|devcrypto|devattest|devattestqcbor|vaultrecover) + expect "guest0 booted the PSA client" "guest0: wolfTrust FF-M psa_framework_version=0x0100" + expect "conformance val_entry start" "guest0: wolfTrust FF-M conformance: val_entry start" + conf_totals + case "$scenario" in + confboot) + # 89 scheduled: 85 pass, the 4 heap tests report SKIPPED on this + # zero-allocation image, as on the H5 and the RT700. + check "$([ "$conf_failed" -eq 0 ] && [ "$conf_passed" -eq 85 ] && [ "$conf_skipped" -eq 4 ]; echo $?)" \ + "Arm FF-M IPC suite: ${conf_passed} passed, ${conf_skipped} skipped, ${conf_failed} failed (want 85/4/0)" + ;; + devstorage) + check "$([ "$conf_failed" -eq 0 ] && [ "$((conf_passed + conf_skipped))" -eq 17 ]; echo $?)" \ + "dev_apis storage: ${conf_passed} passed, ${conf_skipped} skipped, ${conf_failed} failed (17 total)" + ;; + devcrypto|vaultrecover) + check "$([ "$conf_failed" -eq 0 ] && [ "$((conf_passed + conf_skipped))" -eq 77 ]; echo $?)" \ + "dev_apis crypto: ${conf_passed} passed, ${conf_skipped} skipped, ${conf_failed} failed (77 scheduled)" + ;; + devattest|devattestqcbor) + check "$([ "$conf_failed" -eq 0 ] && [ "$conf_passed" -eq 1 ]; echo $?)" \ + "dev_apis initial_attestation: ${conf_passed} passed, ${conf_failed} failed" + ;; + esac + ;; + vaultrecoversec) + refute_re "no fault (fail closed, not a brick)" '\[HARDFLT\]|HardFault|SecureFault' + expect "guest starts under fail-closed attestation" \ + "guest0: wolfTrust FF-M conformance: val_entry start" + ;; + periphneg) + # The CPU leg of the H5 periphneg; the M33MU MCXN947 model has no DMA, so + # Non-secure DMA copies out of Secure memory are silicon work. + faults=$((restart_limit + 1)) + expect_n "guest0 relaunched through its restart budget" \ + "$faults" "wolfTrust MCXN947 guest0: start" + refute_re "no NS read of the SPM's ELS ever returned" "periph probe LEAKED" + expect_n "guest1 launched once, untouched by guest0's faults" 1 \ + "wolfTrust MCXN947 guest1: start" + expect_n "guest1 completed the FF-M handshake" 1 \ + "wolfTrust MCXN947 guest1: FF-M connect ok, done" + stage "boot again with the protection-unit trace, stopping at the first fault" + log="$repo/build/mcxn947_m33mu_${scenario}_trace.log" + M33MU_PROT_TRACE=1 boot_chain 0 "$log" --quit-on-faults + expect "the traced run stopped at a delivered fault" "Execution stopped" + periph_ns="$(printf '0x%08x' $((L3_SPM_PERIPHERAL - 0x10000000)))" + refused="\[MEMFAULT_CAUSE\] sec=NS type=READ addr=$periph_ns reason=secure-attr" + expect_n_re "the fault was guest0's NS read of the SPM's ELS, refused as Secure" \ + 1 "$refused" + sau_refusals="$(grep -A1 -E -- "$refused" "$log" | grep -c "src=SAU" || true)" + check "$([ "$sau_refusals" -eq 1 ]; echo $?)" "the refusal came from the SAU" + ;; + sauneg) + faults=$((restart_limit + 1)) + expect_n "guest0 relaunched through its restart budget" \ + "$faults" "wolfTrust MCXN947 guest0: start" + expect_n "every guest0 launch completed the FF-M handshake before probing" \ + "$faults" "wolfTrust MCXN947 guest0: FF-M connect ok, done" + expect_n "guest1 launched once, untouched by guest0's faults" 1 \ + "wolfTrust MCXN947 guest1: start" + expect_n "guest1 completed the FF-M handshake" 1 \ + "wolfTrust MCXN947 guest1: FF-M connect ok, done" + + # The launch count above shows every attempt faulted; why it faulted comes + # from M33MU's protection-unit trace, which names the attribution behind a + # fault. The trace runs between every console byte, so this boot leaves + # the console off, and it stops at the first delivered fault: the emulator + # is deterministic, so that is guest0's first store. + stage "boot again with the protection-unit trace, stopping at the first fault" + log="$repo/build/mcxn947_m33mu_${scenario}_trace.log" + M33MU_PROT_TRACE=1 boot_chain 0 "$log" --quit-on-faults + expect "the traced run stopped at a delivered fault" "Execution stopped" + refused="\[MEMFAULT_CAUSE\] sec=NS type=WRITE addr=$probe_addr reason=secure-attr" + expect_n_re "the fault was guest0's store into guest1's RAM, refused as Secure" \ + 1 "$refused" + sau_refusals="$(grep -A1 -E -- "$refused" "$log" | grep -c "src=SAU" || true)" + check "$([ "$sau_refusals" -eq 1 ]; echo $?)" \ + "the refusal came from the SAU: the peer window was Secure at dispatch" + ;; +esac + +log "PASS: mcxn947-m33mu/$scenario" diff --git a/tests/target/run_suite.sh b/tests/target/run_suite.sh index d5a506ce..cfa1a6bf 100755 --- a/tests/target/run_suite.sh +++ b/tests/target/run_suite.sh @@ -6,16 +6,18 @@ # tests/target/run_suite.sh m33mu ... -> run_m33mu_scenario.sh # tests/target/run_suite.sh qemu-a ... -> run_qemu_a_scenario.sh # tests/target/run_suite.sh rt700-m33mu ... -> run_rt700_m33mu.sh +# tests/target/run_suite.sh mcxn947-m33mu ... -> run_mcxn947_m33mu.sh # WT_SUITE_LOG_DIR where target-.log lands (default logs) set -u -usage() { echo "usage: $0 m33mu|qemu-a|rt700-m33mu ..." >&2; exit 2; } +usage() { echo "usage: $0 m33mu|qemu-a|rt700-m33mu|mcxn947-m33mu ..." >&2; exit 2; } model="${1:-}" case "$model" in - m33mu) runner=run_m33mu_scenario.sh ;; - qemu-a) runner=run_qemu_a_scenario.sh ;; - rt700-m33mu) runner=run_rt700_m33mu.sh ;; + m33mu) runner=run_m33mu_scenario.sh ;; + qemu-a) runner=run_qemu_a_scenario.sh ;; + rt700-m33mu) runner=run_rt700_m33mu.sh ;; + mcxn947-m33mu) runner=run_mcxn947_m33mu.sh ;; *) usage ;; esac shift diff --git a/tests/target/wolfboot-mcxn947-lifecycle.patch b/tests/target/wolfboot-mcxn947-lifecycle.patch new file mode 100644 index 00000000..6127ffb4 --- /dev/null +++ b/tests/target/wolfboot-mcxn947-lifecycle.patch @@ -0,0 +1,175 @@ +diff -ruN a/hal/mcxn.c b/hal/mcxn.c +--- a/hal/mcxn.c ++++ b/hal/mcxn.c +@@ -25,6 +25,7 @@ + #include "fsl_common.h" + #include "image.h" + #include "hal.h" ++#include "mcxn_lifecycle.h" + + #include "clock_config.h" + #include "fsl_clock.h" +@@ -605,11 +606,22 @@ + return ret; + } + ++/* The PSA security lifecycle the Secure application attests, from the OTP ++ * life cycle state the ROM reflects into SYSCON and the debug authentication ++ * state. */ + int hal_attestation_get_lifecycle(uint32_t *lifecycle) + { ++ uint32_t lcState; ++ uint32_t lcStateDp; ++ uint32_t debugAuthStatus; ++ + if (lifecycle == NULL) + return -1; +- *lifecycle = 0x3000u; /* PSA_LIFECYCLE_SECURED (default) */ ++ lcState = *(volatile uint32_t *)MCXN_SYSCON_ELS_OTP_LC_STATE; ++ lcStateDp = *(volatile uint32_t *)MCXN_SYSCON_ELS_OTP_LC_STATE_DP; ++ debugAuthStatus = *(volatile uint32_t *)MCXN_DAUTHSTATUS_ADDRESS; ++ *lifecycle = mcxn_attestation_lifecycle(lcState, lcStateDp, ++ debugAuthStatus); + return 0; + } + +diff -ruN a/hal/mcxn_lifecycle.h b/hal/mcxn_lifecycle.h +--- a/hal/mcxn_lifecycle.h ++++ b/hal/mcxn_lifecycle.h +@@ -0,0 +1,136 @@ ++/* mcxn_lifecycle.h ++ * ++ * Copyright (C) 2026 wolfSSL Inc. ++ * ++ * This file is part of wolfBoot. ++ * ++ * wolfBoot is free software; you can redistribute it and/or modify ++ * it under the terms of the GNU General Public License as published by ++ * the Free Software Foundation; either version 3 of the License, or ++ * (at your option) any later version. ++ * ++ * wolfBoot is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU General Public License for more details. ++ * ++ * You should have received a copy of the GNU General Public License ++ * along with this program; if not, see . ++ */ ++ ++/* MCXN947 life cycle to PSA security lifecycle. The ROM reflects the OTP ++ * life cycle state into SYSCON (ELS_OTP_LC_STATE and its duplicate); the ++ * state codes are the low byte of the ROM API's nboot_lc_* values. */ ++ ++#ifndef MCXN_LIFECYCLE_H ++#define MCXN_LIFECYCLE_H ++ ++#include ++ ++#define MCXN_SYSCON_ELS_OTP_LC_STATE 0x50000980u ++#define MCXN_SYSCON_ELS_OTP_LC_STATE_DP 0x50000984u ++#define MCXN_LC_STATE_MASK 0xFFu ++#define MCXN_LC_RESERVED_MASK 0xFFFFFF00u ++ ++#define MCXN_LC_NXP_BLANK 0x00u ++#define MCXN_LC_NXP_FAB 0x01u ++#define MCXN_LC_OEM_OPEN 0x03u ++#define MCXN_LC_OEM_SECURE_WORLD_CLOSED 0x07u ++#define MCXN_LC_OEM_CLOSED 0x0Fu ++#define MCXN_LC_OEM_FIELD_RETURN 0x1Fu ++#define MCXN_LC_NXP_FIELD_RETURN 0x7Fu ++#define MCXN_LC_OEM_LOCKED 0xCFu ++#define MCXN_LC_NXP_DEV 0xFCu ++#define MCXN_LC_SHREDDED 0xFFu ++ ++#define MCXN_DAUTHSTATUS_ADDRESS 0xE000EFB8u ++#define MCXN_DAUTHSTATUS_NSID_SHIFT 0u ++#define MCXN_DAUTHSTATUS_NSNID_SHIFT 2u ++#define MCXN_DAUTHSTATUS_SID_SHIFT 4u ++#define MCXN_DAUTHSTATUS_SNID_SHIFT 6u ++#define MCXN_DAUTHSTATUS_FIELD_MASK 0x3u ++#define MCXN_DAUTHSTATUS_ENABLED 0x3u ++#define MCXN_DAUTHSTATUS_DISABLED 0x2u ++ ++static inline uint32_t mcxn_lc_to_psa_lifecycle(uint32_t lcState, ++ uint32_t lcStateDp) ++{ ++ uint32_t lifecycle; ++ ++ if ((lcState & MCXN_LC_RESERVED_MASK) != 0u || ++ (lcStateDp & MCXN_LC_RESERVED_MASK) != 0u || ++ lcState != lcStateDp) { ++ return 0x0000u; /* PSA_LIFECYCLE_UNKNOWN */ ++ } ++ switch (lcState & MCXN_LC_STATE_MASK) { ++ case MCXN_LC_OEM_OPEN: ++ lifecycle = 0x1000u; /* PSA_LIFECYCLE_ASSEMBLY_AND_TEST */ ++ break; ++ case MCXN_LC_OEM_SECURE_WORLD_CLOSED: ++ lifecycle = 0x2000u; /* PSA_LIFECYCLE_PSA_ROT_PROVISIONING */ ++ break; ++ case MCXN_LC_OEM_CLOSED: ++ case MCXN_LC_OEM_LOCKED: ++ lifecycle = 0x3000u; /* PSA_LIFECYCLE_SECURED */ ++ break; ++ case MCXN_LC_OEM_FIELD_RETURN: ++ lifecycle = 0x6000u; /* PSA_LIFECYCLE_DECOMMISSIONED */ ++ break; ++ default: ++ lifecycle = 0x0000u; /* PSA_LIFECYCLE_UNKNOWN */ ++ break; ++ } ++ return lifecycle; ++} ++ ++static inline int mcxn_debug_status_field_is(uint32_t debugAuthStatus, ++ uint32_t shift, uint32_t value) ++{ ++ return (((debugAuthStatus >> shift) & MCXN_DAUTHSTATUS_FIELD_MASK) == ++ value); ++} ++ ++/* Arm encodes each field 0b11 enabled, 0b10 disabled; anything else is ++ * malformed and the whole register is untrusted. */ ++static inline int mcxn_debug_status_field_valid(uint32_t debugAuthStatus, ++ uint32_t shift) ++{ ++ return mcxn_debug_status_field_is(debugAuthStatus, shift, ++ MCXN_DAUTHSTATUS_ENABLED) || ++ mcxn_debug_status_field_is(debugAuthStatus, shift, ++ MCXN_DAUTHSTATUS_DISABLED); ++} ++ ++static inline uint32_t mcxn_attestation_lifecycle(uint32_t lcState, ++ uint32_t lcStateDp, uint32_t debugAuthStatus) ++{ ++ uint32_t lifecycle = mcxn_lc_to_psa_lifecycle(lcState, lcStateDp); ++ ++ if (lifecycle == 0x3000u) { ++ if (!mcxn_debug_status_field_valid(debugAuthStatus, ++ MCXN_DAUTHSTATUS_NSID_SHIFT) || ++ !mcxn_debug_status_field_valid(debugAuthStatus, ++ MCXN_DAUTHSTATUS_NSNID_SHIFT) || ++ !mcxn_debug_status_field_valid(debugAuthStatus, ++ MCXN_DAUTHSTATUS_SID_SHIFT) || ++ !mcxn_debug_status_field_valid(debugAuthStatus, ++ MCXN_DAUTHSTATUS_SNID_SHIFT)) { ++ lifecycle = 0x0000u; /* PSA_LIFECYCLE_UNKNOWN */ ++ } ++ else if (mcxn_debug_status_field_is(debugAuthStatus, ++ MCXN_DAUTHSTATUS_SID_SHIFT, MCXN_DAUTHSTATUS_ENABLED) || ++ mcxn_debug_status_field_is(debugAuthStatus, ++ MCXN_DAUTHSTATUS_SNID_SHIFT, MCXN_DAUTHSTATUS_ENABLED)) { ++ lifecycle = 0x5000u; /* PSA_LIFECYCLE_RECOVERABLE_PSA_ROT_DEBUG */ ++ } ++ else if (mcxn_debug_status_field_is(debugAuthStatus, ++ MCXN_DAUTHSTATUS_NSID_SHIFT, MCXN_DAUTHSTATUS_ENABLED) || ++ mcxn_debug_status_field_is(debugAuthStatus, ++ MCXN_DAUTHSTATUS_NSNID_SHIFT, MCXN_DAUTHSTATUS_ENABLED)) { ++ lifecycle = 0x4000u; /* PSA_LIFECYCLE_NON_PSA_ROT_DEBUG */ ++ } ++ } ++ return lifecycle; ++} ++ ++#endif /* MCXN_LIFECYCLE_H */ From d5ea728412d984e0e3f8c747c7d11f1e57b0a8bd Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 23 Oct 2026 22:04:32 +0100 Subject: [PATCH 05/45] Rehearse the MCXN947 life cycle in provisioning and test its PSA mapping --- tests/host/Makefile | 2 +- tests/host/mcxn947_lifecycle/Makefile | 70 ++++ tests/host/mcxn947_lifecycle/main.c | 136 +++++++ .../provisioning/provisioning_ctrl_mcxn947.sh | 368 ++++++++++++++++++ .../provisioning/test_provisioning_gates.sh | 58 ++- 5 files changed, 632 insertions(+), 2 deletions(-) create mode 100644 tests/host/mcxn947_lifecycle/Makefile create mode 100644 tests/host/mcxn947_lifecycle/main.c create mode 100644 tests/target/provisioning/provisioning_ctrl_mcxn947.sh diff --git a/tests/host/Makefile b/tests/host/Makefile index 127053cd..49d9356f 100644 --- a/tests/host/Makefile +++ b/tests/host/Makefile @@ -33,7 +33,7 @@ endif UNIT_SUITES := domain manifest lifecycle monitor guest_verify rollback sp_recovery \ fabric_windows periph irq_claim rt700_trng rt700_xspi rt700_lifecycle \ - mcxn947_flash \ + mcxn947_flash mcxn947_lifecycle \ ipc ffm spm \ vnet wolfhsm_loopback wolfhsm_relay native_wire native_wire_client \ keystore_isolation \ diff --git a/tests/host/mcxn947_lifecycle/Makefile b/tests/host/mcxn947_lifecycle/Makefile new file mode 100644 index 00000000..04ad3633 --- /dev/null +++ b/tests/host/mcxn947_lifecycle/Makefile @@ -0,0 +1,70 @@ +# 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 + +# The mapping header ships inside the carried wolfBoot patch; test that copy. +PATCH := $(ROOT)/tests/target/wolfboot-mcxn947-lifecycle.patch +HEADER := $(BUILD_DIR)/mcxn_lifecycle.h + +CFLAGS := \ + -I$(BUILD_DIR) \ + -std=c11 -O0 -g -Wall -Wextra -Werror -pedantic +CFLAGS += $(EXTRA_CFLAGS) + +TEST_BIN := $(BUILD_DIR)/test_mcxn947_lifecycle + +.PHONY: all run compilers sanitize valgrind clean + +all: $(TEST_BIN) + +$(BUILD_DIR): + mkdir -p $@ + +$(HEADER): $(PATCH) | $(BUILD_DIR) + awk '/^\+\+\+ b\/hal\/mcxn_lifecycle\.h$$/ { f = 1; next } \ + f && /^diff / { exit } \ + f && /^\+/ { print substr($$0, 2) }' $(PATCH) > $@ + test -s $@ + +$(TEST_BIN): main.c $(HEADER) + $(CC) $(CFLAGS) $(EXTRA_LDFLAGS) -o $@ main.c + +run: $(TEST_BIN) + $(TEST_BIN) + +compilers: + $(MAKE) clean run CC=gcc BUILD_DIR=$(BUILD_DIR)/gcc + $(MAKE) clean run CC=clang BUILD_DIR=$(BUILD_DIR)/clang + +sanitize: + $(MAKE) clean run CC=clang \ + BUILD_DIR=$(BUILD_DIR)/sanitize \ + EXTRA_CFLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" \ + EXTRA_LDFLAGS="-fsanitize=address,undefined" + +valgrind: clean $(TEST_BIN) + valgrind --error-exitcode=1 --leak-check=full --show-leak-kinds=all \ + $(TEST_BIN) + +clean: + rm -rf $(BUILD_DIR) diff --git a/tests/host/mcxn947_lifecycle/main.c b/tests/host/mcxn947_lifecycle/main.c new file mode 100644 index 00000000..5fd432ca --- /dev/null +++ b/tests/host/mcxn947_lifecycle/main.c @@ -0,0 +1,136 @@ +/* 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 . + */ + +/* The MCXN947 life cycle mapping wolfBoot hands wolfTrust, tested from the + * copy inside the carried wolfBoot patch: only an agreeing OEM life cycle + * maps to a PSA state, no NXP-internal, blank or shredded state does, and no + * fault or debug state reads as SECURED. */ + +#include "mcxn_lifecycle.h" + +#include +#include + +static int checks; +static int failures; + +#define EXPECT_LC(actual, expected) \ + do { \ + uint32_t a_ = (actual); \ + uint32_t e_ = (expected); \ + checks++; \ + if (a_ != e_) { \ + (void)fprintf(stderr, "line %d: expected 0x%04x, got 0x%04x\n", \ + __LINE__, (unsigned)e_, (unsigned)a_); \ + failures++; \ + } \ + } while (0) + +/* DAUTHSTATUS: each 2-bit field is 0b11 enabled, 0b10 implemented and disabled. */ +#define DAUTH_CLOSED 0xAAu +#define DAUTH_NS_ONLY 0xABu +#define DAUTH_NSNID 0xAEu +#define DAUTH_SECURE 0xBAu +#define DAUTH_SNID 0xEAu +#define DAUTH_ALL 0xFFu + +static void test_state_table(void) +{ + uint32_t lc; + + EXPECT_LC(mcxn_lc_to_psa_lifecycle(MCXN_LC_OEM_OPEN, MCXN_LC_OEM_OPEN), + 0x1000u); + EXPECT_LC(mcxn_lc_to_psa_lifecycle(MCXN_LC_OEM_SECURE_WORLD_CLOSED, + MCXN_LC_OEM_SECURE_WORLD_CLOSED), 0x2000u); + EXPECT_LC(mcxn_lc_to_psa_lifecycle(MCXN_LC_OEM_CLOSED, + MCXN_LC_OEM_CLOSED), 0x3000u); + EXPECT_LC(mcxn_lc_to_psa_lifecycle(MCXN_LC_OEM_LOCKED, + MCXN_LC_OEM_LOCKED), 0x3000u); + EXPECT_LC(mcxn_lc_to_psa_lifecycle(MCXN_LC_OEM_FIELD_RETURN, + MCXN_LC_OEM_FIELD_RETURN), 0x6000u); + + /* NXP-internal, blank, shredded, and every unlisted code are unknown. */ + for (lc = 0u; lc <= 0xFFu; lc++) { + if (lc == MCXN_LC_OEM_OPEN || lc == MCXN_LC_OEM_SECURE_WORLD_CLOSED || + lc == MCXN_LC_OEM_CLOSED || lc == MCXN_LC_OEM_LOCKED || + lc == MCXN_LC_OEM_FIELD_RETURN) { + continue; + } + EXPECT_LC(mcxn_lc_to_psa_lifecycle(lc, lc), 0x0000u); + } + EXPECT_LC(mcxn_lc_to_psa_lifecycle(MCXN_LC_NXP_BLANK, MCXN_LC_NXP_BLANK), + 0x0000u); + EXPECT_LC(mcxn_lc_to_psa_lifecycle(MCXN_LC_NXP_FAB, MCXN_LC_NXP_FAB), + 0x0000u); + EXPECT_LC(mcxn_lc_to_psa_lifecycle(MCXN_LC_NXP_DEV, MCXN_LC_NXP_DEV), + 0x0000u); + EXPECT_LC(mcxn_lc_to_psa_lifecycle(MCXN_LC_NXP_FIELD_RETURN, + MCXN_LC_NXP_FIELD_RETURN), 0x0000u); + EXPECT_LC(mcxn_lc_to_psa_lifecycle(MCXN_LC_SHREDDED, MCXN_LC_SHREDDED), + 0x0000u); +} + +static void test_redundancy(void) +{ + /* The duplicate must agree, and nothing above the state byte may be set. */ + EXPECT_LC(mcxn_lc_to_psa_lifecycle(MCXN_LC_OEM_OPEN, + MCXN_LC_OEM_SECURE_WORLD_CLOSED), 0x0000u); + EXPECT_LC(mcxn_lc_to_psa_lifecycle(MCXN_LC_OEM_CLOSED, MCXN_LC_OEM_OPEN), + 0x0000u); + EXPECT_LC(mcxn_lc_to_psa_lifecycle(0x00000103u, 0x00000103u), 0x0000u); + EXPECT_LC(mcxn_lc_to_psa_lifecycle(0xFFFC0003u, 0xFFFC0003u), 0x0000u); + EXPECT_LC(mcxn_lc_to_psa_lifecycle(MCXN_LC_OEM_OPEN, 0x00010003u), + 0x0000u); +} + +static void test_debug_status(void) +{ + uint32_t lc = MCXN_LC_OEM_CLOSED; + + EXPECT_LC(mcxn_attestation_lifecycle(lc, lc, DAUTH_CLOSED), 0x3000u); + EXPECT_LC(mcxn_attestation_lifecycle(lc, lc, DAUTH_NS_ONLY), 0x4000u); + EXPECT_LC(mcxn_attestation_lifecycle(lc, lc, DAUTH_NSNID), 0x4000u); + EXPECT_LC(mcxn_attestation_lifecycle(lc, lc, DAUTH_SECURE), 0x5000u); + EXPECT_LC(mcxn_attestation_lifecycle(lc, lc, DAUTH_SNID), 0x5000u); + EXPECT_LC(mcxn_attestation_lifecycle(lc, lc, DAUTH_ALL), 0x5000u); + /* A malformed field (00 or 01) voids the whole register. */ + EXPECT_LC(mcxn_attestation_lifecycle(lc, lc, 0x00u), 0x0000u); + EXPECT_LC(mcxn_attestation_lifecycle(lc, lc, 0xA9u), 0x0000u); + EXPECT_LC(mcxn_attestation_lifecycle(lc, lc, 0x6Au), 0x0000u); + /* Debug state only refines SECURED; the open states ignore it. */ + EXPECT_LC(mcxn_attestation_lifecycle(MCXN_LC_OEM_OPEN, MCXN_LC_OEM_OPEN, + 0x00u), 0x1000u); + EXPECT_LC(mcxn_attestation_lifecycle(MCXN_LC_OEM_SECURE_WORLD_CLOSED, + MCXN_LC_OEM_SECURE_WORLD_CLOSED, DAUTH_ALL), 0x2000u); + EXPECT_LC(mcxn_attestation_lifecycle(MCXN_LC_OEM_FIELD_RETURN, + MCXN_LC_OEM_FIELD_RETURN, DAUTH_ALL), 0x6000u); + EXPECT_LC(mcxn_attestation_lifecycle(MCXN_LC_OEM_LOCKED, + MCXN_LC_OEM_LOCKED, DAUTH_CLOSED), 0x3000u); +} + +int main(void) +{ + test_state_table(); + test_redundancy(); + test_debug_status(); + printf("%s: %d checks, %d failures\n", + (failures == 0) ? "PASS" : "FAIL", checks, failures); + return (failures == 0) ? 0 : 1; +} diff --git a/tests/target/provisioning/provisioning_ctrl_mcxn947.sh b/tests/target/provisioning/provisioning_ctrl_mcxn947.sh new file mode 100644 index 00000000..d182fbbc --- /dev/null +++ b/tests/target/provisioning/provisioning_ctrl_mcxn947.sh @@ -0,0 +1,368 @@ +# shellcheck shell=bash +# provisioning_ctrl_mcxn947.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 . + +# MCXN947 (FRDM-MCXN947) port for provisioning_ctrl.sh: the OTP life cycle +# the ROM reflects into SYSCON, rehearsed by handing wolfTrust the target +# state's PSA life cycle in the boot handoff record (a reset undoes it). +# No permanent write is implemented: every lock is refused. Sourced, never +# run. See docs/MCXN947-Guide.md. +# shellcheck disable=SC2034,SC2154 # PORT_* are read, and $cur/$PENDING set, by the main script + +# A port file only works inside provisioning_ctrl.sh, which holds the gates. +if [ "${BASH_SOURCE[0]}" = "$0" ]; then + echo "REFUSED: run TARGET=mcxn947 tests/target/provisioning/provisioning_ctrl.sh, not this port file." >&2 + exit 2 +fi + +PORT_NAME="MCXN947" +target_dir="$here/.." +pyocd_target="${MCXN947_TARGET:-mcxn947}" +spsdk_venv="${MCXN947_SPSDK_VENV:-$HOME/spsdk-venv}" +# Set here, not in a helper: the controller runs many port calls in subshells. +[ ! -d "$spsdk_venv/bin" ] || PATH="$spsdk_venv/bin:$PATH" +# The Python that runs pyOCD: the venv's, else the one behind pyocd on PATH. +if [ -x "$spsdk_venv/bin/python" ]; then + spsdk_py="$spsdk_venv/bin/python" +else + spsdk_py="$(sed -n '1s/^#! *\([^ ]*python[^ ]*\).*/\1/p' "$(command -v pyocd 2>/dev/null || echo /dev/null)" 2>/dev/null)" + spsdk_py="${spsdk_py:-python3}" +fi +guest_mask="${MCXN947_GUEST_MASK:-0x3}" +case "$guest_mask" in + 0x1|0x2|0x3|1|2|3) ;; + *) refuse "MCXN947_GUEST_MASK must be 0x1, 0x2, or 0x3 (both guests), not '$guest_mask'." ;; +esac +elf="$repo/build/wolftrust.elf" + +# SYSCON life cycle state the ROM reflects from OTP (read-only), its +# duplicate, the debug authentication status, the device UUID (IPC1), the +# BOOT partition's reset vector, and the handoff record. +LC_STATE=0x50000980 +LC_STATE_DP=0x50000984 +DAUTHSTATUS=0xE000EFB8 +UUID_BASE=0x01100000 +WOLFTRUST_RESET_VECTOR=0x10040404 +HANDOFF=0x30030000 +LC_OEM_OPEN=0x03 + +port_ladder() { + cat <<'EOF' +0x03 oem-open no no permanent - +0x07 oem-secure-world yes no permanent 0x03 +0x0F oem-closed yes no permanent 0x07 +0xCF oem-locked yes no permanent 0x0F +0x1F oem-field-return yes no permanent 0x0F +EOF +} +port_usage_extra() { + cat <<'EOF' + verify-wrp report the guest flash fence (none on this port yet) +EOF +} + +lc_hex() { printf '0x%02X' $(( $1 & 0xFF )); } +ensure_pyocd() { + if ! command -v pyocd >/dev/null 2>&1; then + [ -x "$spsdk_venv/bin/pyocd" ] || fail "tools" "pyocd not found (set MCXN947_SPSDK_VENV)" + PATH="$spsdk_venv/bin:$PATH" + export PATH + fi +} +# Words over SWD with the generic attach, which never resets the chip. +read_words() { + local -a cmds=() + local a + for a in "$@"; do cmds+=(-c "read32 $a"); done + timeout 60 pyocd cmd -t cortex_m "${cmds[@]}" 2>&1 | awk '/^[0-9a-f]+:/ { print $2 }' +} +psa_name() { + case "$1" in + 00001000) echo "ASSEMBLY_AND_TEST" ;; 00002000) echo "PSA_ROT_PROVISIONING" ;; + 00003000) echo "SECURED" ;; 00004000) echo "NON_PSA_ROT_DEBUG" ;; + 00005000) echo "RECOVERABLE_PSA_ROT_DEBUG" ;; 00006000) echo "DECOMMISSIONED" ;; + *) echo "UNKNOWN" ;; + esac +} +# The PSA life cycles wolfTrust may report while the life cycle is $1. +expect_psa() { + case "$1" in + 0x03) echo "00001000" ;; 0x07) echo "00002000" ;; + 0x0F|0xCF) echo "00003000 00004000 00005000" ;; 0x1F) echo "00006000" ;; + esac +} +# The PSA life cycle the mock hands wolfTrust for state $1: the production +# value, with debug closed, which only a fused part can show on its own. +mock_psa() { + case "$1" in + 0x07) echo 0x2000 ;; 0x0F|0xCF) echo 0x3000 ;; 0x1F) echo 0x6000 ;; + esac +} +# The life cycle fused when discover ran. +fused_lc() { + local w + w="$(sed -n 's/^LC=\(0x[0-9A-Fa-f]*\) .*/\1/p' "$state_dir/discovery" 2>/dev/null)" + [ -n "$w" ] && lc_hex "$w" +} +elf_sym() { arm-none-eabi-nm "$elf" | awk -v s="$1" '$3 == s && !f { print "0x" $1; f = 1 }'; } +# The life cycle wolfBoot handed wolfTrust: the raw handoff record while it is +# intact, else wolfTrust's consumed copy. +handoff_lifecycle() { + local magic inv lc sym + read -r magic inv lc < <(read_words "$HANDOFF" $((HANDOFF + 4)) $((HANDOFF + 12)) | tr '\n' ' '; echo) + if [ "${magic:-}" = "5742484f" ] && [ $((0x$magic ^ 0x${inv:-0})) -eq $((0xFFFFFFFF)) ]; then + echo "$lc" + return 0 + fi + [ -s "$elf" ] || return 1 + sym="$(elf_sym g_boot_lifecycle)" + [ -n "$sym" ] || return 1 + read_words "$sym" +} +# The four images the chain flashes, as "address file" lines (physical +# offsets; wolfBoot at 0, the BOOT partition and both guest windows). +flashed_images() { + printf '%s %s\n' \ + 0x00000000 "${MCXN947_WORK:-$repo/build/mcxn947}/flash_wolfboot.bin" \ + 0x00040000 "$repo/build/wolftrust_v1_signed.bin" \ + 0x00080000 "$repo/tests/firmware/mcxn947-baremetal/build/guest0.bin" \ + 0x00100000 "$repo/tests/firmware/mcxn947-baremetal/build/guest1.bin" +} +port_image_digest() { framed_digest; } +images_on_device() { + local a f + while read -r a f; do + timeout 120 pyocd cmd -t cortex_m \ + -c "savemem $a $(wc -c < "$f" | tr -d ' ') $state_dir/readback.bin" \ + >/dev/null 2>&1 && cmp -s "$state_dir/readback.bin" "$f" || return 1 + done < <(flashed_images) +} +# The device UUID, readable over SWD in every state short of closed debug. +chip_uuid() { + local w + w="$(read_words "$UUID_BASE" $((UUID_BASE + 4)) $((UUID_BASE + 8)) $((UUID_BASE + 12)) | tr -d '\n')" + [ ${#w} -eq 32 ] && [ "$w" != "00000000000000000000000000000000" ] && + [ "$w" != "ffffffffffffffffffffffffffffffff" ] && echo "$w" +} +# The one attached debug probe; the FRDM board's MCU-Link is soldered on. +probe_uid() { + local ids + ids="$(timeout 30 pyocd list 2>/dev/null | awk '$1 ~ /^[0-9]+$/ { print $(NF-1) }')" + [ "$(printf '%s\n' "$ids" | grep -c .)" = "1" ] && echo "$ids" +} +# The MCXN947 has no guest flash fence yet: the array is written only through +# the ROM API from the Secure side, and no hardware write fence is claimed. +fence_line() { + echo "none (no guest flash fence on this port; a WT_GUEST_FLASH_WRP=1 image refuses every guest)" + return 1 +} + +port_status() { + local lc lcd dauth hl vm rf uuid + ensure_pyocd + read -r lc lcd dauth < <(read_words "$LC_STATE" "$LC_STATE_DP" "$DAUTHSTATUS" | tr '\n' ' '; echo) + echo "OTP life cycle LC_STATE=0x${lc: -2} ($(state_name "$(lc_hex "0x$lc")" || true)) LC_STATE_DP=0x${lcd: -2}" + echo "DAUTHSTATUS 0x$dauth" + echo "device UUID $(chip_uuid || echo unreadable)" + echo "guest fence $(fence_line || true)" + if hl="$(handoff_lifecycle)"; then echo "wolfTrust saw 0x$hl ($(psa_name "$hl"))"; fi + if [ -s "$elf" ]; then + read -r vm rf < <(read_words "$(elf_sym g_wt_launch_verified_mask)" \ + "$(elf_sym g_wt_launch_refused_mask)" | tr '\n' ' '; echo) + echo "guest launches verified=0x${vm:-?} refused=0x${rf:-?}" + fi +} +port_discover() { + local lc lcd dauth hl uuid + ensure_pyocd + mkdir -p "$state_dir" + rm -f "$state_dir/discovery" "$state_dir/regress-ok" + read -r lc lcd dauth < <(read_words "$LC_STATE" "$LC_STATE_DP" "$DAUTHSTATUS" | tr '\n' ' '; echo) + [ -n "${lc:-}" ] && [ -n "${lcd:-}" ] || fail "discover" "cannot read the life cycle over SWD" + [ "$(lc_hex "0x$lc")" = "$(lc_hex "0x$lcd")" ] || fail "discover" "life cycle copies disagree (LC 0x$lc, DP 0x$lcd)" + [ $((0x$lc & 0xFFFFFF00)) -eq 0 ] && [ $((0x$lcd & 0xFFFFFF00)) -eq 0 ] || + fail "discover" "life cycle words 0x$lc/0x$lcd carry bits above the state byte" + case "$(lc_hex "0x$lc")" in + 0x03|0x07|0x0F) ;; + *) fail "discover" "life cycle $(lc_hex "0x$lc") has no further rehearsal step" ;; + esac + pass "life cycle is $(label "$(lc_hex "0x$lc")") and its duplicate agrees" + uuid="$(chip_uuid)" || fail "discover" "cannot read the device UUID at $UUID_BASE" + pass "device UUID $uuid" + hl="$(handoff_lifecycle)" || fail "discover" "no readable boot handoff: boot the wolfTrust chain first" + pass "the boot handoff life cycle is readable (0x$hl, $(psa_name "$hl"))" + printf 'LC=0x%s DP=0x%s DAUTH=0x%s UUID=%s\n' "$lc" "$lcd" "$dauth" "$uuid" > "$state_dir/discovery" + echo "PASS: discovery stamped ($state_dir/discovery)" +} +port_restore() { + fail "restore" "no MCXN947 hardware runner yet: flash wolfBoot, the signed wolfTrust image and both guests by hand (see the MCXN947 Guide), then run 'discover'" +} +port_extra() { + local line + case "$1" in + verify-wrp) + line="$(fence_line)" || fail "verify-wrp" "$line" + pass "guest fence $line" ;; + provision-da|burn) + refuse "'$1' would program OTP: this port rehearses life cycles only and writes none of them; see the MCXN947 Guide." ;; + set-perimeter|set-wrp|clear-wrp) + refuse "'$1' has no MCXN947 form: wolfTrust programs the TrustZone perimeter on every boot, every reset clears it, and the port has no guest flash fence yet." ;; + *) return 1 ;; + esac +} + +port_advance_check() { + [ -s "$state_dir/discovery" ] || refuse "run 'discover' first." + [ "$1" = "0x07" ] || [ -s "$state_dir/regress-ok" ] || + refuse "prove 'regress' from oem-secure-world before advancing to $1." +} +# Halt at wolfTrust's reset vector, after wolfBoot wrote the handoff record +# and before wolfTrust consumes it, replace the record's PSA life cycle with +# the target state's, and resume. The fuses and SYSCON are untouched. +port_advance() { + local entry vm rf psa + ensure_pyocd + vm="$(elf_sym g_wt_launch_verified_mask)"; rf="$(elf_sym g_wt_launch_refused_mask)" + [ -n "$vm" ] && [ -n "$rf" ] || fail "advance" "launch masks not found in $elf" + entry="$(read_words "$WOLFTRUST_RESET_VECTOR")" + [ -n "$entry" ] && [ "$entry" != "00000000" ] && [ "$entry" != "ffffffff" ] || + fail "advance" "no wolfTrust reset vector at $WOLFTRUST_RESET_VECTOR (flash the chain first)" + psa="$(mock_psa "$1")" + echo "ADVANCING the boot handoff life cycle to $psa for $1 ($(state_name "$1")); regress or any reset undoes it" + "$spsdk_py" - "0x$entry" "$psa" "$HANDOFF" "$pyocd_target" "$vm" "$rf" <<'PYEOF' +import sys +import time +from pyocd.core.helpers import ConnectHelper +from pyocd.core.target import Target + +HANDOFF_MAGIC = 0x5742484F +entry, psa, handoff = (int(a, 0) for a in sys.argv[1:4]) +with ConnectHelper.session_with_chosen_probe( + options={"target_override": sys.argv[4], "resume_on_disconnect": True, + "reset_type": "hw"}) as s: + t = s.target + t.reset_and_halt() + t.set_breakpoint(entry & ~1) + t.resume() + deadline = time.time() + 10 + while t.get_state() != Target.State.HALTED or \ + t.read_core_register("pc") != (entry & ~1): + if time.time() > deadline: + sys.exit("wolfTrust entry breakpoint not reached") + time.sleep(0.02) + t.remove_breakpoint(entry & ~1) + if t.read32(handoff) != HANDOFF_MAGIC or \ + t.read32(handoff + 4) != (HANDOFF_MAGIC ^ 0xFFFFFFFF): + sys.exit("no intact handoff record at 0x%08x" % handoff) + t.write32(handoff + 12, psa) + got = t.read32(handoff + 12) + # SRAM survives the reset: clear the launch masks so only this boot's + # wolfTrust can set them. + for a in sys.argv[5:7]: + t.write32(int(a, 0), 0) + if t.read32(int(a, 0)) != 0: + sys.exit("could not clear the boot evidence at %s" % a) + t.resume() +print("halted at wolfTrust's entry 0x%08x; handoff life cycle=0x%04x" % (entry & ~1, got)) +sys.exit(0 if got == psa else 3) +PYEOF + sleep 3 +} +# The rehearsal evidence: the life cycle wolfTrust saw, every guest launched +# verified, the images read back, and the device UUID. +port_booted() { + local hl want vm rf digest uuid + # advance cleared the launch masks, so both are this boot's. + hl="$(handoff_lifecycle)" || { echo "no boot handoff life cycle readable after the advance"; return 1; } + echo "wolfTrust saw 0x$hl ($(psa_name "$hl"))" + case " $(expect_psa "$1") " in + *" $hl "*) pass "wolfTrust booted with the $(state_name "$1") life cycle" ;; + *) echo "wolfTrust saw 0x$hl, not the life cycle of $1"; return 1 ;; + esac + # Field Return is decommissioned: wolfTrust need not launch guests there. + if [ "$1" != "0x1F" ]; then + want=$((guest_mask)) + for _ in 1 2 3 4 5 6 7 8 9 10; do + read -r vm rf < <(read_words "$(elf_sym g_wt_launch_verified_mask)" \ + "$(elf_sym g_wt_launch_refused_mask)" | tr '\n' ' '; echo) + [ -n "${vm:-}" ] && [ $(((0x$vm | 0x${rf:-0}) & want)) -eq "$want" ] && break + sleep 1 + done + [ -n "${vm:-}" ] && [ $((0x$vm)) -eq "$want" ] && [ $((0x${rf:-1})) -eq 0 ] || { + echo "guests did not all launch (verified=0x${vm:-?} refused=0x${rf:-?}, want $guest_mask)" + return 1 + } + pass "guests launched (verified=0x$vm refused=0x$rf)" + fi + digest="$(port_image_digest)" || { echo "missing a flashed image"; return 1; } + images_on_device || { echo "the images on the part differ from the host build: flash the chain again"; return 1; } + pass "the images on the part match the host build (${digest:0:16})" + uuid="$(chip_uuid)" || { echo "cannot read the device UUID"; return 1; } + EVIDENCE="id=$uuid image=$digest fused=$(fused_lc) fence=none" +} +port_regress() { + local fused lc lcd hl + ensure_pyocd + timeout 60 pyocd reset -t "$pyocd_target" -m hw >/dev/null 2>&1 || + fail "regress" "hardware reset through pyocd failed" + sleep 3 + fused="$(fused_lc || echo "$LC_OEM_OPEN")" + read -r lc lcd < <(read_words "$LC_STATE" "$LC_STATE_DP" | tr '\n' ' '; echo) + [ "$(lc_hex "0x$lc")" = "$fused" ] && [ "$(lc_hex "0x$lcd")" = "$fused" ] || + fail "regress" "life cycle after reset is 0x$lc/0x$lcd, not the fused $fused (run discover)" + pass "hardware reset reloaded the fused $(state_name "$fused") life cycle" + hl="$(handoff_lifecycle)" || fail "regress" "cannot read the life cycle wolfTrust saw after the reset" + case " $(expect_psa "$fused") " in + *" $hl "*) pass "wolfTrust booted $(psa_name "$hl") again" ;; + *) fail "regress" "wolfTrust saw 0x$hl after regress" ;; + esac + # Only a regress out of a recorded mock proves the way back for later steps. + if [ -n "$PENDING" ]; then + mkdir -p "$state_dir" + date -u +%FT%TZ > "$state_dir/regress-ok" + fi +} + +port_cred_fp() { echo none; } +port_lock_current() { + local lc lcd + ensure_pyocd + read -r lc lcd < <(read_words "$LC_STATE" "$LC_STATE_DP" | tr '\n' ' '; echo) + [ -n "${lc:-}" ] && [ -n "${lcd:-}" ] || refuse "cannot read the life cycle over SWD." + [ "$(lc_hex "0x$lc")" = "$(lc_hex "0x$lcd")" ] || refuse "the life cycle copies disagree (LC 0x$lc, DP 0x$lcd)." + lc_hex "0x$lc" +} +port_lock_identity() { chip_uuid || true; } +port_rehearsal_for() { echo "$1"; } +port_record_ok() { + local rfused + rfused="$(field fused "$1")" + [ -n "$rfused" ] && [ $((rfused & ~cur & 0xFF)) -eq 0 ] && + [ "$(field fence "$1")" = "none" ] && + [ "$(chip_uuid || true)" = "$(field id "$1")" ] +} +# No life cycle write is implemented for this part: the ladder marks every +# state unlockable, and these refuse again should the ladder ever change. +port_ready() { + refuse "$(label "$1") cannot be locked by this port: the MCXN947 life cycle write (ROM ISP, OTP) is not validated here, so only rehearsals run; see the MCXN947 Guide." +} +port_lock_plan() { refuse "no MCXN947 life cycle write is implemented."; } +port_consequence() { echo "the OTP life cycle cannot be unburned, and the part never returns to $(label "$cur")."; } +port_lock_write() { refuse "no MCXN947 life cycle write is implemented."; } +port_lock_verify() { refuse "no MCXN947 life cycle write is implemented."; } diff --git a/tests/target/provisioning/test_provisioning_gates.sh b/tests/target/provisioning/test_provisioning_gates.sh index 0792f926..efc410a4 100755 --- a/tests/target/provisioning/test_provisioning_gates.sh +++ b/tests/target/provisioning/test_provisioning_gates.sh @@ -93,7 +93,21 @@ esac EOF cat > "$T/venv/bin/pyocd" </dev/null; exit 0; fi +if [ "\$1" = cmd ]; then + # With a register file (MCXN947), answer each read32 from it; else the + # RT700 SFP dump. + if [ -s "$T/mcxn_regs" ]; then + for a in "\$@"; do + case "\$a" in + "read32 "*) k=\$(printf '%08x' "\${a#read32 }" 2>/dev/null || echo 0) + v=\$(awk -v k="\$k" '\$1 == k { print \$2 }' "$T/mcxn_regs") + echo "\$k: \${v:-00000000}" ;; + esac + done + exit 0 + fi + cat "$T/sfp" 2>/dev/null; exit 0 +fi [ "\$1" = list ] || exit 0 echo " # Probe/Board Unique ID Target" echo "-----------------------------------" @@ -360,5 +374,47 @@ check "the next step after the burn" 2 "runs only from in-field (0x0F)" -- env " rec 0x0F 0x07 "$(edig)" armed "$(now)" check "In Field refused until ROM authentication" 2 "needs the BootROM to authenticate wolfBoot" -- env "${ISP[@]}" "${RT[@]}" lock in-field nowrite + +echo "== MCXN947" +MX=(env TARGET=mcxn947 MCXN947_SPSDK_VENV="$T/venv" WT_PROVISION_STATE="$T/st" PATH="$T/venv/bin:$PATH" "$P") +mkdir -p "$R/build/mcxn947" "$R/tests/firmware/mcxn947-baremetal/build" +echo mwb > "$R/build/mcxn947/flash_wolfboot.bin" +echo mg0 > "$R/tests/firmware/mcxn947-baremetal/build/guest0.bin" +echo mg1 > "$R/tests/firmware/mcxn947-baremetal/build/guest1.bin" +# regs [DAUTHSTATUS]: the words the pyocd stub answers +# (SYSCON life cycle, debug status, UUID, an intact handoff, the reset vector). +regs() { + printf '50000980 %08x\n50000984 %08x\ne000efb8 %08x\n' $(($1)) $(($2)) $((${3:-0xAA})) > "$T/mcxn_regs" + printf '01100000 11111111\n01100004 22222222\n01100008 33333333\n0110000c 44444444\n' >> "$T/mcxn_regs" + printf '30030000 5742484f\n30030004 a8bdb7b0\n3003000c 00001000\n10040404 10040801\n' >> "$T/mcxn_regs" +} +regs 0x03 0x03 +check "help lists the port's states" 0 "oem-secure-world" -- "${MX[@]}" help +check "the MCXN947 port file refuses to run alone" 2 "not this port file" -- bash "$R/tests/target/provisioning/provisioning_ctrl_mcxn947.sh" lock 0x07 +for m in 0 0x4 junk; do + check "MCXN947_GUEST_MASK=$m refused" 2 "MCXN947_GUEST_MASK must be" -- env MCXN947_GUEST_MASK="$m" WT_LOCK_CONFIRM=1 "${MX[@]}" advance 0x07 +done +check "no guest flash fence is reported" 1 "no guest flash fence" -- "${MX[@]}" verify-wrp +check "lock refused: oem-secure-world" 2 "oem-secure-world (0x07) is not a lock target" -- "${MX[@]}" lock 0x07 +check "lock refused: oem-closed by name" 2 "oem-closed (0x0F) is not a lock target" -- "${MX[@]}" lock oem-closed +check "lock refused: oem-locked" 2 "is not a lock target" -- "${MX[@]}" lock 0xCF +check "lock refused: oem-field-return" 2 "is not a lock target" -- "${MX[@]}" lock 0x1F +check "lock refused with the production opt-in too" 2 "is not a lock target" -- env WT_FIXTURE_BOUND=1 WT_LOCK_CONFIRM=1 WT_PRODUCTION_LOCK=1 "${MX[@]}" lock 0x07 +check "advance needs discover" 2 "run 'discover' first" -- env WT_LOCK_CONFIRM=1 "${MX[@]}" advance 0x07 +check "status reads the life cycle and the UUID" 0 "device UUID 11111111222222223333333344444444" -- "${MX[@]}" status +regs 0x03 0x07 +check "discover: copies disagree" 1 "life cycle copies disagree" -- "${MX[@]}" discover +regs 0x103 0x103 +check "discover: bits above the state byte" 1 "above the state byte" -- "${MX[@]}" discover +regs 0xCF 0xCF +check "discover: no further rehearsal step" 1 "has no further rehearsal step" -- "${MX[@]}" discover +regs 0x03 0x03 +check "discover stamps the life cycle and UUID" 0 "discovery stamped" -- "${MX[@]}" discover +check "advance past oem-secure-world needs a regress" 2 "prove 'regress' from oem-secure-world" -- env WT_LOCK_CONFIRM=1 "${MX[@]}" advance 0x0F +check "OTP writes refused" 2 "would program OTP" -- "${MX[@]}" burn +check "perimeter commands refused" 2 "has no MCXN947 form" -- "${MX[@]}" set-wrp +check "restore has no runner yet" 1 "no MCXN947 hardware runner" -- env WT_LOCK_CONFIRM=1 "${MX[@]}" restore +nowrite +rm -f "$T/mcxn_regs" echo "provisioning gates: $pass passed, $failn failed" [ "$failn" = 0 ] From 9e77b2ef5c9d0a76220cc872ed2d7c8ff19d0a8f Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 23 Oct 2026 22:04:32 +0100 Subject: [PATCH 06/45] Run the MCXN947 port in CI and make test-target --- .github/workflows/README.md | 13 ++++++++---- .github/workflows/cross-compile.yml | 21 ++++++++++++++++++- .github/workflows/m33mu.yml | 31 ++++++++++++++++------------- Makefile | 6 ++++-- README.md | 10 ++++++---- 5 files changed, 56 insertions(+), 25 deletions(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 368cdf15..b1ccd428 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -6,7 +6,7 @@ matrix on labels, main pushes, and nightly. `codeql.yml` runs C security queries. `coverity.yml` runs on Sundays at 00:00 UTC or by manual dispatch. Both build the host suites and secure images -for STM32H563 and MIMXRT700 with both crypto engines. +for STM32H563, MIMXRT700, and MCXN947 with both crypto engines. Coverity scans wolfTrust runtime, ports, tests, and generated policy code. `tools/ci-coverity-filter.py` removes dependency source units before upload @@ -21,8 +21,8 @@ An accepted upload queues analysis; results appear after Coverity processes it. | Tier | Trigger | Purpose | |------|---------|---------| | **Fast (per-PR)** | every PR; push to main | host unit suites, ISO C99, house style, bare-scope scan, Arm PSA-FF conformance, cross-compile, compiler matrix, sanitizers, valgrind, integrations, core/port split guard | -| **M33MU smoke** | every PR | per port, on both crypto engines: STM32H563 `positive gtzcneg crossdomain bothpsa confboot devcrypto`; MIMXRT700 `positive ahbscneg crossdomain bothpsa confboot devcrypto` | -| **M33MU full** | PR labels `ci:all`, `ci:h5`, `ci:rt700`; push to main; `cron: 0 8 * * *`; `workflow_dispatch` (port input) | every scenario of that port on both engines (see below) | +| **M33MU smoke** | every PR | per port, on both crypto engines: STM32H563 `positive gtzcneg crossdomain bothpsa confboot devcrypto`; MIMXRT700 `positive ahbscneg crossdomain bothpsa confboot devcrypto`; MCXN947 `positive sauneg crossdomain bothpsa confboot devcrypto` | +| **M33MU full** | PR labels `ci:all`, `ci:h5`, `ci:rt700`, `ci:mcxn947`; push to main; `cron: 0 8 * * *`; `workflow_dispatch` (port input) | every scenario of that port on both engines (see below) | The M33MU workflow (`m33mu.yml`) is label-selected the way wolfProvider's `pr-osp-select.yml` is: a `select` job checks the routing table @@ -35,6 +35,7 @@ groups it picked, so a job that is not selected never appears as skipped. | (no label) | each port's smoke tier on both engines | | `ci:h5` | the STM32H563 full matrix (the change touched only that port) | | `ci:rt700` | the MIMXRT700 full matrix (the change touched only that port) | +| `ci:mcxn947` | the MCXN947 full matrix (the change touched only that port) | | `ci:all` / `ci:m33mu` | every scenario of every port (a core change) | A label keeps applying on later pushes to the PR. Pushes to @@ -74,10 +75,14 @@ test-target` also reads. The table below is a representative slice: | `mimxrt700_devstorage_devattest_devattestqcbor_` | `devstorage devattest devattestqcbor` | PSA ITS/PS and Initial Attestation conformance (the token parses under wolfCOSE's shim and reference QCBOR) | | `mimxrt700_devcrypto_vaultrecover_vaultrecoversec_` | `devcrypto vaultrecover vaultrecoversec` | PSA Crypto conformance on wolfPSA; a foreign vault pool self-heals in development and fails closed when SECURED | | `mimxrt700_rollbackneg_remeasureneg_manifestneg_spbudgetneg_` | `rollbackneg remeasureneg manifestneg spbudgetneg` | shared Secure-verdict table: rollback refusal, re-measure tamper, corrupted manifest, restart budget | +| `mcxn947_positive_` | `positive` | MCXN947 chain under the MCXN947 model (internal flash, ROM flash API, EdgeLock entropy), both guests finish | +| `mcxn947_sauneg_` | `sauneg` | guest0's store into guest1's RAM refused by the SAU, contained | +| `mcxn947__` | as the MIMXRT700 groups above | the same scenario groups on the MCXN947 model; `tests/target/lib/scenario_matrix.py` is the list | An unlabeled PR runs only each port's smoke tier; the full matrix needs a `ci:` label. To run a single scenario locally, use `tests/target/run_m33mu_scenario.sh ` (the -RT700 checks use `tests/target/run_rt700_m33mu.sh `); to run a port's +RT700 checks use `tests/target/run_rt700_m33mu.sh `, the MCXN947 checks +`tests/target/run_mcxn947_m33mu.sh `); to run a port's full matrix on a PR, add its `ci:` label; off-PR against a branch, `workflow_dispatch` on `m33mu.yml` with the `port` input. diff --git a/.github/workflows/cross-compile.yml b/.github/workflows/cross-compile.yml index d6bf6f18..676e8894 100644 --- a/.github/workflows/cross-compile.yml +++ b/.github/workflows/cross-compile.yml @@ -49,7 +49,7 @@ jobs: echo "LTO flash: $no_lto_flash -> $lto_flash bytes" - name: Verify an unimplemented isolation level stops the build run: | - for target in stm32h563 mimxrt700; do + for target in stm32h563 mimxrt700 mcxn947; do for level in 1 2; do if make TARGET=$target WT_ISOLATION_LEVEL=$level \ BUILD_DIR=build-level-$level secure-image \ @@ -119,3 +119,22 @@ jobs: WT_CONFORMANCE=1 secure-image TOOLPREFIX=arm-none-eabi- \ BUILD_DIR=build_rt700_conf_$engine done + - name: Build the MCXN947 secure image (both engines, default layout) + run: | + for engine in native hsm; do + make TARGET=mcxn947 WT_ENGINE=$engine WT_ATTEST_COSE=0 \ + secure-image TOOLPREFIX=arm-none-eabi- \ + BUILD_DIR=build_mcxn947_$engine + # The default must leave wolfBoot's 0x400 header before the vectors. + vectors="$(arm-none-eabi-objdump -h build_mcxn947_$engine/wolftrust.elf \ + | awk '$2 == ".vectors" { print $4 }')" + test "$vectors" = "10040400" || \ + { echo "MCXN947 .vectors at 0x$vectors, want 0x10040400"; exit 1; } + done + - name: Build the MCXN947 conformance secure image (both engines) + run: | + for engine in native hsm; do + make TARGET=mcxn947 WT_ENGINE=$engine WT_ATTEST_COSE=0 \ + WT_CONFORMANCE=1 secure-image TOOLPREFIX=arm-none-eabi- \ + BUILD_DIR=build_mcxn947_conf_$engine + done diff --git a/.github/workflows/m33mu.yml b/.github/workflows/m33mu.yml index 68d84ea1..b206d3bf 100644 --- a/.github/workflows/m33mu.yml +++ b/.github/workflows/m33mu.yml @@ -1,9 +1,9 @@ name: M33MU on: - # Pull requests run each port's smoke tier; the ci:all, ci:h5, and ci:rt700 - # labels, pushes to main, the nightly schedule (workflow_call from - # nightly.yml), and manual dispatch run a port's full matrix. + # Pull requests run each port's smoke tier; the ci:all, ci:h5, ci:rt700, and + # ci:mcxn947 labels, pushes to main, the nightly schedule (workflow_call + # from nightly.yml), and manual dispatch run a port's full matrix. pull_request: types: [opened, synchronize, reopened, labeled] push: @@ -13,7 +13,7 @@ on: port: description: Port whose full matrix to run type: choice - options: [all, stm32h563, mimxrt700] + options: [all, stm32h563, mimxrt700, mcxn947] default: all workflow_call: @@ -278,7 +278,7 @@ jobs: fi # Label-selected, the wolfProvider way: one select job reads the ci:* labels - # (ci:h5 or ci:rt700 for that port's full matrix when only that port + # (ci:h5, ci:rt700 or ci:mcxn947 for that port's full matrix when only that port # changed, ci:all or ci:m33mu for everything after a core change, no label # for each port's smoke tier), the event, and the dispatch input, then one # matrix job runs exactly those scenario groups, so nothing shows up skipped. @@ -324,27 +324,30 @@ jobs: git config --global --add safe.directory "$GITHUB_WORKSPACE" git config --global url."https://github.com/".insteadOf git@github.com: - # The RT700 wolfBoot first stage builds against the MCUXpresso SDK, staged - # the way wolfBoot's own imxrt700 emulator workflow stages it. + # The RT700 and MCXN947 wolfBoot first stages build against the + # MCUXpresso SDK, staged the way wolfBoot's own imxrt700 emulator + # workflow stages it, one board per port. - uses: actions/checkout@v4 - if: matrix.port == 'mimxrt700' + if: matrix.port == 'mimxrt700' || matrix.port == 'mcxn947' with: repository: nxp-mcuxpresso/CMSIS_5 ref: f9cf5df6d51a95f0146930d75af3d71977cf795a path: CMSIS_5 - name: Cache MCUXpresso SDK - if: matrix.port == 'mimxrt700' + if: matrix.port == 'mimxrt700' || matrix.port == 'mcxn947' id: cache-mcuxpresso uses: actions/cache@v4 with: path: | mcuxpresso-sdk venv - key: mcuxpresso-sdk-mimxrt700evk-${{ hashFiles('.github/workflows/m33mu.yml') }} + key: mcuxpresso-sdk-${{ matrix.port == 'mimxrt700' && 'mimxrt700evk' || 'frdmmcxn947' }}-${{ hashFiles('.github/workflows/m33mu.yml') }} - name: Setup MCUXpresso SDK - if: matrix.port == 'mimxrt700' && steps.cache-mcuxpresso.outputs.cache-hit != 'true' + if: (matrix.port == 'mimxrt700' || matrix.port == 'mcxn947') && steps.cache-mcuxpresso.outputs.cache-hit != 'true' + env: + SDK_BOARD: ${{ matrix.port == 'mimxrt700' && 'mimxrt700evk' || 'frdmmcxn947' }} run: | python3 -m venv venv . venv/bin/activate @@ -352,12 +355,12 @@ jobs: west init -m https://github.com/nxp-mcuxpresso/mcuxsdk-manifests.git \ --mr v26.06.00-LTS mcuxpresso-sdk cd mcuxpresso-sdk - sed -i '/se_hostlib\|emwin/d' manifests/boards/mimxrt700evk.yml - west update_board --set board mimxrt700evk + sed -i '/se_hostlib\|emwin/d' "manifests/boards/$SDK_BOARD.yml" + west update_board --set board "$SDK_BOARD" - name: Run target scenarios env: - MCUXSDK: ${{ matrix.port == 'mimxrt700' && '1' || '' }} + MCUXSDK: ${{ (matrix.port == 'mimxrt700' || matrix.port == 'mcxn947') && '1' || '' }} MCUXPRESSO: ${{ github.workspace }}/mcuxpresso-sdk/mcuxsdk MCUXPRESSO_CMSIS: ${{ github.workspace }}/CMSIS_5/CMSIS run: tests/target/run_suite.sh ${{ matrix.arm }} ${{ matrix.key }} diff --git a/Makefile b/Makefile index 256b8830..28919f15 100644 --- a/Makefile +++ b/Makefile @@ -72,8 +72,8 @@ c99-check: # FF-M target scenarios for one port under the M33MU emulator, separate from # `make test` (host-only). TARGET picks the port (stm32h563 by default, -# mimxrt700 for the RT700 chain, whose runner builds its own pinned emulator -# and wolfBoot first stage); WT_TIER=smoke (default) runs the per-PR set, +# mimxrt700 or mcxn947 for the NXP chains, whose runners build their own +# pinned emulator and wolfBoot first stage); WT_TIER=smoke (default) runs the per-PR set, # WT_TIER=full every scenario, both from tests/target/lib/scenario_matrix.py. # The STM32H563 path auto-detects an M33MU (or WT_TARGET_SCENARIOS=1) and skips # explicitly otherwise so it never silently passes. Runs inside the @@ -84,6 +84,8 @@ WT_SCENARIOS = $(shell python3 tests/target/lib/scenario_matrix.py \ test-target: ifeq ($(TARGET),mimxrt700) @tests/target/run_suite.sh rt700-m33mu $(WT_SCENARIOS) +else ifeq ($(TARGET),mcxn947) + @tests/target/run_suite.sh mcxn947-m33mu $(WT_SCENARIOS) else @if ! tests/target/detect_m33mu.sh >/dev/null 2>&1; then \ echo "SKIP: FF-M target scenarios ($$(tests/target/detect_m33mu.sh 2>&1))"; \ diff --git a/README.md b/README.md index dbef3bf3..0f45bbb6 100644 --- a/README.md +++ b/README.md @@ -88,13 +88,14 @@ current reference port, not a requirement imposed on every intended port. ## Ports -wolfTrust currently supports two ports; each guide covers the board, the +wolfTrust currently supports three ports; each guide covers the board, the first-stage loader contract, and the runners: - STM32H563 (NUCLEO-H563ZI): [STM32H5 Guide](docs/STM32H5-Guide.md) - NXP MIMXRT700 (MIMXRT700-EVK): [MIMXRT700 Guide](docs/MIMXRT700-Guide.md) +- NXP MCXN947 (FRDM-MCXN947): [MCXN947 Guide](docs/MCXN947-Guide.md) -Both build with `TARGET=` (`stm32h563` is the default) and run the +All build with `TARGET=` (`stm32h563` is the default) and run the same scenario set under the M33MU emulator with `make test-target TARGET=`. @@ -137,14 +138,15 @@ Common validation entry points: make test make test-target make test-target TARGET=mimxrt700 +make test-target TARGET=mcxn947 make test-conformance WT_H5_DOCKER_IMAGE=ghcr.io/wolfssl/wolfboot-ci-m33mu:v1.15 make test-hardware ``` `make test-target` runs the port's smoke tier under M33MU (`WT_TIER=full` for every scenario): the STM32H563 default skips explicitly when M33MU is -unavailable, and `TARGET=mimxrt700` builds its pinned emulator and wolfBoot -first stage itself. +unavailable, and `TARGET=mimxrt700` or `TARGET=mcxn947` builds its pinned +emulator and wolfBoot first stage itself. `make test-conformance` instead runs its 20-test host subset and warns that it is not full emulator or hardware evidence. `make test-hardware` skips when board detection fails; on hosts without `lsusb`, a missing ST-Link can instead From 95f5f59821e0daecd6f0c75774a88ed0c5707acb Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 23 Oct 2026 22:04:32 +0100 Subject: [PATCH 07/45] Document the MCXN947 port --- docs/Home.md | 1 + docs/MCXN947-Guide.md | 312 ++++++++++++++++++++++++++++++++++++++++++ docs/Porting.md | 6 +- docs/Provisioning.md | 38 ++--- docs/Testing.md | 44 +++++- docs/_Sidebar.md | 1 + 6 files changed, 380 insertions(+), 22 deletions(-) create mode 100644 docs/MCXN947-Guide.md diff --git a/docs/Home.md b/docs/Home.md index eebff2c3..9a1565b3 100644 --- a/docs/Home.md +++ b/docs/Home.md @@ -118,3 +118,4 @@ port's five CMSE gateway veneers. | [Project Structure](Project-Structure.md) | Repository layout | | [STM32H5 Guide](STM32H5-Guide.md) | STM32H563 provisioning, flashing, WRP, and recovery safety | | [MIMXRT700 Guide](MIMXRT700-Guide.md) | MIMXRT700 provisioning, flashing, XSPI guest fence, and recovery safety | +| [MCXN947 Guide](MCXN947-Guide.md) | MCXN947 internal flash, ELS entropy, emulator runner, life cycle rehearsal, and open items | diff --git a/docs/MCXN947-Guide.md b/docs/MCXN947-Guide.md new file mode 100644 index 00000000..d2f3b57c --- /dev/null +++ b/docs/MCXN947-Guide.md @@ -0,0 +1,312 @@ +# MCXN947 Guide + +This guide covers the NXP FRDM-MCXN947 (MCXN947, Cortex-M33 CPU0 with +TrustZone-M) as the third Armv8-M port. Unlike the MIMXRT700 it executes from +2 MiB of internal ECC flash, programmed through the boot ROM's flash API, and +its entropy comes from the EdgeLock (ELS) block. CPU1 is Non-secure only and +out of scope. Changing the OTP life cycle or the CMPA can permanently lock +the part; this port rehearses life cycles and writes none of them. + +## Status + +- **Validated on silicon:** the wolfBoot first stage for this part + (wolfSSL/wolfBoot#948: signed boot with ECC256/SHA-256, update swap and + rollback over the write-once trailer, and the measured Secure-application + handoff to `0x30030000`). +- **Validated in cross-build:** the wolfTrust Secure image + (`make TARGET=mcxn947 secure-image`) on both crypto engines and both + manifests, with the port split, veneer, owner map, and layout checks green. +- **Validated in emulation:** the chain under M33MU's MCXN947 model + (`tests/target/run_mcxn947_m33mu.sh`, see Testing): wolfBoot verifies the + Secure image, both bare-metal guests launch and complete their PSA calls + through the veneers (`positive`, both engines), the isolation negative + (`sauneg`) shows a guest's store into the other guest's RAM refused by the + SAU, contained by the monitor, and the offender quarantined after its + restart budget while the peer keeps running, and the isolation level 3 + scenarios the matrix lists for this port + (`tests/target/lib/scenario_matrix.py`) pass on the model. Scenarios the + matrix exempts have not been run. +- **Not validated:** nothing of the wolfTrust Secure image has run on the + FRDM board yet; there is no hardware runner, no guest flash fence, and no + life cycle write. The TRDC and the AHB secure controller are left as the + first loader leaves them. See [Open items](#open-items). + +Record emulator, cross-build, and physical-board evidence separately: M33MU's +MCXN947 model gives emulator evidence, the FRDM board gives silicon evidence, +and neither substitutes for the other. + +## What an MCXN947 port comprises + +### First-stage loader (wolfBoot) + +| Addition | Purpose | +| --- | --- | +| `hal/mcxn.{c,ld}` | LPUART4 debug console and the internal flash through the ROM flash API (`FLASH_Init`, `FLASH_Program`, `FLASH_Erase`, `FLASH_VerifyErase`); the flash path runs from RAM. | +| `config/examples/mcxn-tz-wolftrust.config` | TrustZone enabled with the generic Secure-application handoff (`WOLFBOOT_SECURE_APP`): wolfBoot writes the measured-boot record to Secure SRAM and stays in Secure state across the jump; 8 KiB sectors, a 1024-byte image header, `NVM_FLASH_WRITEONCE`. | +| `tests/target/wolfboot-mcxn947-lifecycle.patch` (carried here) | reads the OTP life cycle the ROM reflects into `SYSCON.ELS_OTP_LC_STATE` and its duplicate, with `DAUTHSTATUS`, and hands wolfTrust the matching PSA life cycle; the HAL at the pin reports a fixed SECURED value. | + +The loader satisfies the [Porting](Porting.md) bootloader contract: it +authenticates the Secure image, provides `wt_boot_handoff_t` (SHA-256 +measurement, life cycle, image version) at `0x30030000`, reserves the +1024-byte image header, and provides an update partition compatible with the +firmware-update backend. + +### Secure runtime (wolfTrust) + +The port reuses `src/arch/armv8m/`, `src/arch/common/`, and the shared +Armv8-M isolation level 3 layer in `port/common/armv8m/` unchanged, and adds +`port/mcxn947/` and one build fragment: + +| File | Responsibility | +| --- | --- | +| `memory_map.h` | The bit-28 Secure-alias map: flash windows, Secure and guest RAM, the boot-handoff address, and the port-sized level 3 bands (`WT_L3_SPM_RAM_SIZE`, `WT_SP_HSM_DATA_SIZE`) from `port/common/armv8m/l3_layout.h`. | +| `l3_port.h` | The level 3 layer's one board input: the ELS as the Secure peripheral only the SPM drives. | +| `mcxn947_regs.h` | Register bases for SYSCON, the FMU, the ELS, LPUART4, the TRDC and the AHBSC, the boot ROM and its API tree, and the ELS command interface. | +| `platform_mcxn947.c` | Every `wt_platform_*` operation: the SAU table, the Secure MPU whitelist (eight regions, one of them the boot ROM for the flash API), the boot-handoff region, fault logging, panic, and reset. | +| `partitions.c` | The guest and capability tables, the profile capability bitmap (the fabric filter is claimed on the per-dispatch SAU window, as on the MIMXRT700), and the pinned guest-measurement slot. | +| `rom_flash.c/.h` | Internal flash program and erase through the boot ROM's API tree, called directly with a local definition of the driver interface layout (no vendor SDK): one 16-byte phrase is programmed at most once per erase, every erase is verified by the ROM, and the code-bus cache is cleared after each change. | +| `hsm_flash.c/.h`, `hsm_flash_ctx.c` | The `port_nvm.h` backend for the wolfHSM store and the FWU staging backend: reads through the Secure alias, program and erase through `rom_flash.c`. | +| `rng_entropy.c` | The `CUSTOM_RAND_GENERATE_BLOCK` entropy source over the ELS `RND_REQ` command, preserving the unprivileged-to-privileged trap. | +| `secure.ld` | The port's Secure linker script, which INCLUDEs the shared level 3 regions, band sections, and layout ASSERTs; the SG veneers sit in the image's second 1 KiB, as on the STM32H5. | +| `secure_owners.txt` | The port's objects in the post-link owner map, appended to `tools/secure_owners.txt` by the link. | +| `manifest.json`, `manifest-conformance.json`, `conformance/` | The service partitions and their resources; the conformance manifest and PAL bindings host Arm's test partitions. | +| `mk/target-mcxn947.mk` | `WT_CPU`, the flash and RAM defaults, the linker `--defsym` set, and the source lists. | + +## Reference flash and RAM layout + +Flash is `0x00000000` (Non-secure) / `0x10000000` (Secure); RAMA-H SRAM is +`0x20000000` / `0x30000000`; peripherals `0x40000000` / `0x50000000`. Bit 28 +selects the Secure alias. Flash is two 1 MiB banks (read-while-write across +banks) of 8 KiB sectors and 16-byte ECC phrases. + +| Image or region | Non-secure | Secure alias | +| --- | ---: | ---: | +| wolfBoot (bank 0) | `0x00000000` | `0x10000000` | +| wolfTrust Secure image, BOOT partition (`0x40000`) | `0x00040000` | `0x10040000` | +| Guest 0 (`0x80000`) | `0x00080000` | `0x10080000` | +| Guest 1 (`0x40000`, bank 1) | `0x00100000` | `0x10100000` | +| wolfBoot update partition (`0x40000`) | `0x00180000` | `0x10180000` | +| wolfBoot swap sector (`0x2000`) | `0x001C0000` | `0x101C0000` | +| wolfHSM NVM store (two mirrored sectors) | `0x001E0000` | `0x101E0000` | +| Conformance NVM store (one sector) | `0x001E4000` | `0x101E4000` | + +| RAM region | Address | +| --- | ---: | +| Guest 0 RAM (`0x18000`) | `0x20000000` | +| Guest 1 RAM (`0x18000`) | `0x20018000` | +| Boot-handoff record (4 KiB page, the record is 56 bytes) | `0x30030000` | +| Secure runtime RAM (`0x37000`: 86 KiB SPM, 42 KiB keystore bands, 102 KiB stacks and conformance data) | `0x30031000` | + +The guest windows alias wolfBoot's own RAM (`0x30000000` to `0x3001E000`), +dead after the jump; `wt_platform_init` zeroes both windows before any guest +runs. Every store below the Secure image is in bank 1, so the image keeps +executing from bank 0 while the ROM API programs the NVM and update +partitions: no RAM code band is needed, and the port has none. + +These constants come from `port/mcxn947/memory_map.h` and +`mk/target-mcxn947.mk`; the emulator runner assembles the flash image from +them so the build and flash offsets stay paired. + +## TrustZone perimeter + +The MCXN947 has no option-byte Secure watermark. The Secure boundary is set at +run time by what the port programs before any guest launches: + +- **IDAU/SAU:** the bit-28 alias makes each address inherently Secure or + Non-secure; the SAU table in `platform_mcxn947.c` opens the static + Non-secure windows (the guest flash, the LPUART4 console) and the + Non-secure-callable veneer window in the Secure image, and leaves + everything else Secure. The guest RAM windows are not static: each dispatch + programs a dynamic SAU region over the arriving guest's window only, so the + peer's window stays Secure while it runs, exactly as on the MIMXRT700. +- **Secure MPU:** a per-partition whitelist confines each Secure Partition to + its own RAM band. The SPM's eight regions are the Secure image, the Secure + alias of everything above it (guest images, update and swap partitions, the + stores: read-only, execute-never), Secure RAM from the handoff record up, + the guest RAM alias, the flash Non-secure alias, the peripheral aperture, + the private peripheral bus, and the boot ROM (privileged read-execute, for + the flash API). +- **TRDC and AHBSC:** not programmed. At reset the flash MBC leaves every + block Non-secure-accessible and the AHB secure controller rules are open, + as the first loader leaves them. The port claims the fabric filter on the + per-dispatch SAU window alone (`partitions.c`): it isolates CPU0's guests + from each other and says nothing about CPU1, DMA, or any other bus master. + No capability is claimed for fencing those. + +As on the other ports, the manifest declares the guests unprivileged but the +runtime launches them with `CONTROL_NS.nPRIV` clear; a guest's Non-secure MPU +is scheduling policy, not a boundary. The SAU window is the boundary, and +`sauneg` shows it under M33MU. + +## Silicon constraints for this port + +- **A phrase is written once.** The flash has ECC per 16-byte phrase; a + programmed phrase cannot be reprogrammed until its sector is erased. The + wolfHSM store runs with `WOLFHSM_CFG_FLASH_UNIT_SIZE` 16 (the wolfTrust + runner setting all ports share), so every journal unit is one phrase and + no unit shares a phrase with another written at a different time; + `rom_flash.c` refuses to program a non-blank phrase and `hsm_flash.c` + refuses any program that is not phrase-aligned. `tests/host/mcxn947_flash` + runs the real driver and the wolfHSM store over a model of this flash and + proves it: 554 phrase programs across three partition compactions and a + reopen, zero double programs with 16-byte units (even through a backend + with no phrase guard), 281 double programs with wolfHSM's 8-byte default, + which the port backend refuses outright. +- **Flash is programmed through the ROM.** There is no SDK in the build; the + port calls the ROM API tree at `0x1303FC00` through a local definition of + its layout, checks the geometry the ROM reports (8 KiB sectors, 2 MiB) + before the first program, masks interrupts around each call, and clears + the LPCAC afterwards. Reads, verifies, and blank checks go through the + Secure alias, as wolfBoot's own flash path does on silicon. +- **Entropy is the ELS.** `RND_REQ` (command 24) fills a buffer by DMA; the + port requests whole pages, serves only what a request uses, wipes the rest, + and fails closed on `ELS_ERR` or a locked ELS. The ROM leaves the ELS + clocked; the port enables it and never resets it. +- **The first loader's image header sets the Secure link address.** wolfBoot + on this part uses a 1024-byte image header, so wolfTrust links at the boot + base plus `0x400` (`WT_SECURE_IMAGE_HEADER_SIZE=0x400`) and is signed with + `IMAGE_HEADER_SIZE=1024` and 8 KiB sectors. +- **NSC in flash works.** The flash is in the Code region, where the IDAU + honours an SAU Non-secure-callable window, so the veneers stay in the + image, as on the STM32H5 (the MIMXRT700's RAM code band is not needed). + +## Required tools + +- `arm-none-eabi-gcc` with newlib headers, and `arm-none-eabi-{nm,objcopy,size}` +- Python 3, CMake (for the emulator build) +- wolfBoot's MCXN HAL builds against the MCUXpresso SDK (`MCUXPRESSO`, + `MCUXPRESSO_CMSIS`, or `~/NXP` as wolfBoot's config expects) +- for a board: the FRDM-MCXN947 with its MCU-Link, pyOCD with the MCXN947 + pack, and SPSDK's `blhost` (`tests/target/detect_mcxn947.sh` reports what + is missing); no hardware runner exists yet + +## Build and verify under M33MU + +```sh +make TARGET=mcxn947 secure-image +make test-target TARGET=mcxn947 +tests/target/run_mcxn947_m33mu.sh positive +tests/target/run_mcxn947_m33mu.sh sauneg +``` + +The runner builds its pinned emulator (upstream M33MU plus +`tests/target/m33mu-mcxn947.patch`, which adds the ELS `RND_REQ` register +model and reflects the OEM_OPEN life cycle into SYSCON) and its pinned +wolfBoot (`tests/target/lib/mcxn947_wolfboot.sh`: the wolfSSL/wolfBoot#948 +branch at its fork ref until it merges, built from +`config/examples/mcxn-tz-wolftrust.config` with the carried life cycle +patch), then: + +1. builds the wolfTrust Secure image and its CMSE import library with the + port's default `WT_SECURE_IMAGE_HEADER_SIZE=0x400`; +2. builds the bare-metal guest for both guest windows, linked against the + Secure image's CMSE import library; +3. patches both guest measurement records into the unsigned `wolftrust.bin` + (`tools/measure/patch_guest_digests.py`); +4. signs `wolftrust.bin` with the wolfBoot key tools (`IMAGE_HEADER_SIZE=1024`, + `WOLFBOOT_SECTOR_SIZE=0x2000`); +5. boots `m33mu --cpu mcxn947` with wolfBoot at flash offset 0, the signed + Secure image at `0x40000`, and the guests at `0x80000` and `0x100000`. + +`MCXN947_WOLFBOOT_DIR` reuses a wolfBoot tree the library built; `M33MU` a +prebuilt emulator carrying the patch. + +### Bring-up markers + +Each guest records progress in a mailbox at the base of its own Non-secure RAM +window (`0x20000000` for guest 0, `0x20018000` for guest 1) and echoes it on +LPUART4 (`0x400B4000`): + +| Mailbox word | Offset | Pass value | +| --- | ---: | ---: | +| signature | `+0x00` | `0x47543030` | +| step | `+0x04` | `0x00000005` | +| `psa_framework_version()` | `+0x08` | `0x00000100` | +| `psa_connect(SERVICE_HSM)` handle | `+0x10` | > 0, distinct per guest | +| status | `+0x14` | `0x600D600D` | +| LPUART4 `VERID` as the guest reads it | `+0x18` | non-zero | +| isolation probe latch (`sauneg`) | `+0x1C` | `1` faulted or `2` blocked; `3` leaked | +| isolation probe read-back (`sauneg`) | `+0x20` | not the stored sentinel | + +A `status` of `0x600D600D` in both mailboxes proves the wolfBoot to wolfTrust +to Non-secure-guest chain booted and that the Secure runtime serviced both +Non-secure PSA clients through the veneers. In `sauneg`, guest 0 disables its +own Non-secure MPU and stores a sentinel into guest 1's RAM; the runner +asserts from M33MU's protection-unit trace that the store faulted, refused by +the SAU with the peer window Secure at dispatch. + +## Provisioning and life cycle + +This section is the MCXN947 part of [Provisioning](Provisioning.md). The +life cycle lives in OTP; the ROM reflects it into `SYSCON.ELS_OTP_LC_STATE` +(`0x50000980`) and its duplicate (`0x50000984`), eight bits each, which the +carried wolfBoot patch reads and maps to the PSA life cycle it hands +wolfTrust (`tests/host/mcxn947_lifecycle` tests that mapping): + +| `LC_STATE` | NXP state | PSA life cycle wolfBoot hands wolfTrust | +| --- | --- | --- | +| `0x03` | OEM Open (as the FRDM board ships) | `0x1000` ASSEMBLY_AND_TEST | +| `0x07` | OEM Secure World Closed | `0x2000` PSA_ROT_PROVISIONING | +| `0x0F` | OEM Closed | `0x3000` SECURED with every `DAUTHSTATUS` field disabled; `0x5000`/`0x4000` while debug is open; `0x0000` for a malformed `DAUTHSTATUS` | +| `0xCF` | OEM Locked | as OEM Closed | +| `0x1F` | OEM Field Return | `0x6000` DECOMMISSIONED | +| other, or the copies disagree | NXP Blank, Fab, Dev, Provisioned, Field Return, Shredded, or corrupt | `0x0000` UNKNOWN | + +All commands use the single entry point with `TARGET=mcxn947`: + +```sh +export TARGET=mcxn947 +tests/target/provisioning/provisioning_ctrl.sh status +tests/target/provisioning/provisioning_ctrl.sh discover +WT_LOCK_CONFIRM=1 tests/target/provisioning/provisioning_ctrl.sh advance 0x07 +WT_LOCK_CONFIRM=1 tests/target/provisioning/provisioning_ctrl.sh regress +``` + +What differs from the MIMXRT700: + +- **The mock lock is in the handoff record.** The MCXN947 has no life cycle + shadow register (`ELS_OTP_LC_STATE` is read-only), so `advance` halts the + core at wolfTrust's reset vector, after wolfBoot wrote the handoff record + and before wolfTrust consumed it, and replaces the record's PSA life cycle + with the target state's production value (`0x2000`, `0x3000`, or + `0x6000`). Fuses and SYSCON are untouched; `regress` is a hardware reset. + The rehearsal records what wolfTrust saw, the guests launched verified, + the images read back, and the device UUID (`0x01100000`). +- **No state can be locked.** The port implements no OTP write: every + `lock`, `burn`, and `provision-da` is refused, and `verify-wrp` reports + that there is no guest flash fence. The real life cycle advance on this + part (the ROM's ISP path) is not validated here. +- **`restore` has no runner yet.** Flash wolfBoot, the signed Secure image, + and both guests by hand, then `discover`. + +`make test-provisioning` runs the MCXN947 gates against stub tools with the +other ports' gates. + +## Open items + +- **Silicon bring-up.** The Secure image, the ELS entropy path, the ROM + flash API calls from the privileged SVC gate, the LPCAC clear, and the + Secure MPU map (including executing the ROM through its Secure alias) are + emulator-validated only. The first board run should check the attribution + of every Secure peripheral the SPM uses and that reading an erased phrase + through the alias returns `0xFF` without a fault, which wolfBoot's flash + path on this part already relies on. +- **Guest flash fence.** No hardware write protection of the guest windows is + claimed: `wt_platform_guest_flash_wrp_ok` reports every window unprotected, + so a `WT_GUEST_FLASH_WRP=1` image refuses every guest. The TRDC MBC gates + bus accesses to the flash, not the FMU's program and erase commands, so it + is not the fence; the candidate is a flash page lock the ROM's + `kStatus_FLASH_ModifyProtectedAreaDisallowed` status names, which needs the + reference manual and a silicon negative before it can be claimed. +- **Other bus masters.** CPU1, DMA, and the ELS DMA are not fenced from + Secure RAM; programming the TRDC and the AHB secure controller for them is + silicon work that M33MU does not model. +- **Hardware runner and lock.** `run_mcxn947_hardware.sh`, the hardware + suite, and a validated life cycle write do not exist yet. +- **Upstream pins.** The wolfBoot pin moves to wolfSSL/wolfBoot once + wolfSSL/wolfBoot#948 merges; the emulator patch goes once M33MU models the + ELS command interface and seeds the life cycle itself. + +See [Porting](Porting.md) for the generic port contract, [Testing](Testing.md) +for scenario selection, and [Security Model](Security-Model.md) for the policy +enforced after boot. diff --git a/docs/Porting.md b/docs/Porting.md index 633fed6b..67f00f3a 100644 --- a/docs/Porting.md +++ b/docs/Porting.md @@ -4,8 +4,10 @@ wolfTrust separates reusable policy and services from architecture, device, and board-specific execution. The fully silicon-validated build tuple is `armv8m-stm32h563`. A second Armv8-M tuple, `armv8m-mimxrt700` (external octal-NOR execute-in-place), is in hardware bring-up and reuses the -architecture adapter unchanged; see the [STM32H5 Guide](STM32H5-Guide.md) and -[MIMXRT700 Guide](MIMXRT700-Guide.md) for the two worked examples. Support for +architecture adapter unchanged, as does a third, `armv8m-mcxn947` (internal +ECC flash through the boot ROM's flash API, emulator-validated); see the +[STM32H5 Guide](STM32H5-Guide.md), [MIMXRT700 Guide](MIMXRT700-Guide.md), and +[MCXN947 Guide](MCXN947-Guide.md) for the worked examples. Support for additional Cortex-M ports is an intended extension point. Such ports may reuse common policy and service code and an existing architecture adapter when their execution and protection models match. diff --git a/docs/Provisioning.md b/docs/Provisioning.md index 02219bd9..9a35918f 100644 --- a/docs/Provisioning.md +++ b/docs/Provisioning.md @@ -11,13 +11,15 @@ One script does this on every port, with the same commands: TARGET= tests/target/provisioning/provisioning_ctrl.sh [state] ``` -`TARGET` is `stm32h563` (the default) or `mimxrt700`. `help` lists the -commands and the port's states. Only the state codes and a few device commands -differ between ports. This page covers the shared flow; each port guide covers -its states, quirks, and a walkthrough with real output: +`TARGET` is `stm32h563` (the default), `mimxrt700`, or `mcxn947`. `help` lists +the commands and the port's states. Only the state codes and a few device +commands differ between ports. This page covers the shared flow; each port +guide covers its states, quirks, and a walkthrough with real output: - [STM32H5 Guide: Provisioning and product state](STM32H5-Guide.md#provisioning-and-product-state) - [MIMXRT700 Guide: Provisioning and life cycle](MIMXRT700-Guide.md#provisioning-and-life-cycle) +- [MCXN947 Guide: Provisioning and life cycle](MCXN947-Guide.md#provisioning-and-life-cycle) + (rehearsal only: this port locks nothing) > **Production locks are permanent.** `lock` is for a production station and a > part you intend to ship, never a development board. Every step before it is @@ -177,21 +179,21 @@ with it can never be updated; a leaked one means they trust whoever holds it. ## PSA life cycle by port -| PSA life cycle | STM32H5 product state | MIMXRT700 life cycle | -| --- | --- | --- | -| `0x1000` ASSEMBLY_AND_TEST | open `0xED` | develop `0x03` | -| `0x2000` PSA_ROT_PROVISIONING | provisioning `0x17` | develop2 `0x07` | -| `0x4000` NON_PSA_ROT_DEBUG | tz-closed `0xC6` | in-field, only Non-secure debug open | -| `0x5000` RECOVERABLE_PSA_ROT_DEBUG | closed, Secure debug open | in-field, Secure debug open | -| `0x3000` SECURED | closed `0x72`, locked `0x5C` | in-field `0x0F`, in-field-locked `0xCF` | -| `0x6000` DECOMMISSIONED | none | in-field-return `0x1F` | -| `0x0000` UNKNOWN | any other value | copies disagree, or an NXP-internal state | +| PSA life cycle | STM32H5 product state | MIMXRT700 life cycle | MCXN947 life cycle | +| --- | --- | --- | --- | +| `0x1000` ASSEMBLY_AND_TEST | open `0xED` | develop `0x03` | oem-open `0x03` | +| `0x2000` PSA_ROT_PROVISIONING | provisioning `0x17` | develop2 `0x07` | oem-secure-world `0x07` | +| `0x4000` NON_PSA_ROT_DEBUG | tz-closed `0xC6` | in-field, only Non-secure debug open | oem-closed, only Non-secure debug open | +| `0x5000` RECOVERABLE_PSA_ROT_DEBUG | closed, Secure debug open | in-field, Secure debug open | oem-closed, Secure debug open | +| `0x3000` SECURED | closed `0x72`, locked `0x5C` | in-field `0x0F`, in-field-locked `0xCF` | oem-closed `0x0F`, oem-locked `0xCF` | +| `0x6000` DECOMMISSIONED | none | in-field-return `0x1F` | oem-field-return `0x1F` | +| `0x0000` UNKNOWN | any other value | copies disagree, or an NXP-internal state | copies disagree, or an NXP-internal state | ## Environment | Variable | Meaning | | --- | --- | -| `TARGET` | the port: `stm32h563` (default) or `mimxrt700` | +| `TARGET` | the port: `stm32h563` (default), `mimxrt700`, or `mcxn947` | | `WT_LOCK_CONFIRM=1` | allow a board write; without it `lock` only previews | | `WT_PRODUCTION_LOCK=1` | marks a production station; `lock` never writes without it | | `WT_FIXTURE_BOUND=1` | the fixture holds one part from rehearsal to lock; needed where the part's identity cannot be read | @@ -200,6 +202,7 @@ with it can never be updated; a leaked one means they trust whoever holds it. | `WT_DA_OBK`, `WT_DA_KEY`, `WT_DA_CERT`, `WT_DA_PWD` | STM32H5 Debug Authentication inputs; a production lock requires all four, none of them ST's sample | | `STM32_CLI`, `H5_SERIAL` | STM32H5: STM32CubeProgrammer CLI and the board UART | | `RT700_ISP`, `RT700_SPSDK_VENV`, `RT700_GUEST_MASK` | MIMXRT700: blhost ISP connection, SPSDK environment, guests a rehearsal must launch | +| `MCXN947_SPSDK_VENV`, `MCXN947_GUEST_MASK`, `MCXN947_TARGET` | MCXN947: the pyOCD environment, guests a rehearsal must launch, and the pyOCD target name | ## Adding a port @@ -222,6 +225,7 @@ cannot weaken them. ## Testing -`make test` runs `make test-provisioning`: every gate of both ports against -stub vendor tools, from the generic refusals to the typed acceptance. Nothing -touches a board; the typed-acceptance cases need `expect` and skip without it. +`make test` runs `make test-provisioning`: every gate of the three ports +against stub vendor tools, from the generic refusals to the typed acceptance. +Nothing touches a board; the typed-acceptance cases need `expect` and skip +without it. diff --git a/docs/Testing.md b/docs/Testing.md index 258bb17e..b6e6526c 100644 --- a/docs/Testing.md +++ b/docs/Testing.md @@ -392,6 +392,39 @@ These are emulator results. They prove the SAU attribution and the monitor's containment on a faithful core model; the silicon `ahbscneg` run on the EVK is recorded separately. +## MCXN947 emulator scenarios + +M33MU also models the MCXN947 (`--cpu mcxn947`: internal flash, the boot ROM +flash API tree, and, with `tests/target/m33mu-mcxn947.patch`, the ELS +`RND_REQ` register model and the OEM_OPEN life cycle in SYSCON), so the +MCXN947 port has the same emulator gate as the MIMXRT700, with the same +scenario names: + +```sh +make test-target TARGET=mcxn947 +tests/target/run_mcxn947_m33mu.sh positive +tests/target/run_mcxn947_m33mu.sh sauneg +``` + +`sauneg` is the MIMXRT700's `ahbscneg` on this part: guest 0 stores into +guest 1's RAM window, which the per-dispatch SAU window keeps Secure, the +SAU refuses the store, and the monitor contains it. Every other scenario is +the MIMXRT700 one run on the MCXN947 model, with the port's own band, stack, +and SPM peripheral addresses read from `port/mcxn947/memory_map.h`; the +scenarios the port runs, and the level 3 scenarios it still exempts, are in +`tests/target/lib/scenario_matrix.py`. The runner builds its own pinned +emulator and wolfBoot first stage (`tests/target/lib/mcxn947_wolfboot.sh`: +the wolfSSL/wolfBoot#948 branch at its fork ref until it merges, from +`config/examples/mcxn-tz-wolftrust.config` plus +`tests/target/wolfboot-mcxn947-lifecycle.patch`, the life cycle read the +attestation service needs); building it needs the MCUXpresso SDK as any MCXN +wolfBoot build does. `MCXN947_WOLFBOOT_DIR` reuses a tree the library built. +The flash image offsets are physical: wolfBoot at 0, the signed Secure image +at `0x40000`, the guests at `0x80000` and `0x100000`. + +These are emulator results; nothing of the wolfTrust chain has run on the +FRDM-MCXN947 yet, and the port has no hardware runner. + ## MIMXRT700 hardware `make test-hardware TARGET=mimxrt700` runs `tests/target/run_rt700_suite.sh` @@ -502,7 +535,10 @@ The workflows under `.github/workflows/` separately run: home-directory references in the published docs); - fuzz targets; - selected and nightly M33MU scenarios; and -- the MIMXRT700 chain under M33MU's RT700 model (the `RT700` scenario matrix). +- the MIMXRT700 chain under M33MU's RT700 model (the `RT700` scenario matrix); + and +- the MCXN947 chain under M33MU's MCXN947 model (the `MCXN947` scenario + matrix). ### Trigger routing @@ -516,11 +552,13 @@ that picks each port's tier and one matrix job that runs exactly those scenario groups) is tiered. Every pull request runs each port's smoke tier on both crypto engines (STM32H563: `positive`, `gtzcneg`, `crossdomain`, `bothpsa`, `confboot`, `devcrypto`; MIMXRT700: -`positive`, `ahbscneg`, `crossdomain`, `bothpsa`, `confboot`, `devcrypto`). +`positive`, `ahbscneg`, `crossdomain`, `bothpsa`, `confboot`, `devcrypto`; +MCXN947: `positive`, `sauneg`, `crossdomain`, `bothpsa`, `confboot`, +`devcrypto`). The full matrix runs on a push to `main`, on the nightly schedule, on manual dispatch (with a `port` input), and on a pull request that carries the -`ci:h5`, `ci:rt700`, or `ci:all` label. The scenario groups per port and tier +`ci:h5`, `ci:rt700`, `ci:mcxn947`, or `ci:all` label. The scenario groups per port and tier are in `tests/target/lib/scenario_matrix.py`; `make test-target TARGET=` runs the same smoke tier locally and `WT_TIER=full` every scenario. diff --git a/docs/_Sidebar.md b/docs/_Sidebar.md index 5a180ccd..fa313200 100644 --- a/docs/_Sidebar.md +++ b/docs/_Sidebar.md @@ -18,3 +18,4 @@ - [[Project Structure]] - [[STM32H5 Guide]] - [[MIMXRT700 Guide]] +- [[MCXN947 Guide]] From 14085f99adc1eaf2039fe5b3e27d781b94ee4898 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 23 Oct 2026 22:06:37 +0100 Subject: [PATCH 08/45] Shorten the MCXN947 port comments --- port/mcxn947/hsm_flash.c | 9 +++------ port/mcxn947/memory_map.h | 5 ++--- port/mcxn947/partitions.c | 14 ++++++-------- port/mcxn947/platform_mcxn947.c | 16 ++++++---------- port/mcxn947/secure.ld | 16 ++++++---------- tests/firmware/mcxn947-baremetal/guest0.c | 16 ++++++---------- tests/host/mcxn947_flash/main.c | 11 +++-------- tests/host/mcxn947_lifecycle/main.c | 5 ++--- .../provisioning/provisioning_ctrl_mcxn947.sh | 6 ++---- 9 files changed, 36 insertions(+), 62 deletions(-) diff --git a/port/mcxn947/hsm_flash.c b/port/mcxn947/hsm_flash.c index 5fe42feb..c54aabf2 100644 --- a/port/mcxn947/hsm_flash.c +++ b/port/mcxn947/hsm_flash.c @@ -373,12 +373,9 @@ int wt_hsm_flash_format(void) return wt_hsm_flash_erase(&g_hsm_flash_ctx, 0u, g_hsm_flash_cfg.size); } -/* SERVICE_FWU staging into the wolfBoot update partition: the FWU partition - * erases each target sector lazily, programs the candidate through the ROM - * API, and verifies every block against the Secure alias. Staging must be - * contiguous from offset 0, so a finished candidate has no unwritten gap. - * arm() programs wolfBoot's update trigger in the trailer so the next boot - * swaps; the swapped image is still gated by authenticated launch. */ +/* SERVICE_FWU staging into the wolfBoot update partition, contiguous from + * offset 0 with each sector erased lazily; arm() programs wolfBoot's update + * trigger in the trailer, and the swapped image still faces launch verify. */ /* Erased high-water mark: every sector below it is blank or holds staged * data and is never erased again, whatever the partition's sector count. */ diff --git a/port/mcxn947/memory_map.h b/port/mcxn947/memory_map.h index 09509fe2..0a280c12 100644 --- a/port/mcxn947/memory_map.h +++ b/port/mcxn947/memory_map.h @@ -84,9 +84,8 @@ #define WT_GUEST_RAM_SIZE 0x00018000u /* Secure runtime RAM: the top 220 KiB of RAMA-H through its Secure alias, - * 4 KiB above the wolfBoot handoff record (a 56-byte record; the rest of - * that page is scratch the core clears once the record is consumed). The - * isolation level 3 bands are offsets from this base. */ + * one 4 KiB page above the wolfBoot handoff record (56 bytes, the page is + * cleared once consumed); the level 3 bands are offsets from this base. */ #define WT_RAM_S_BASE 0x30031000u #define WT_RAM_S_SIZE 0x00037000u #define WT_BOOT_HANDOFF_ADDRESS 0x30030000u diff --git a/port/mcxn947/partitions.c b/port/mcxn947/partitions.c index abb0e3ea..797b97d0 100644 --- a/port/mcxn947/partitions.c +++ b/port/mcxn947/partitions.c @@ -26,17 +26,15 @@ #include -/* Pinned guest-measurement slot. The image-assembly patcher locates it by - * magic inside wolftrust.bin, stamps the guest digests, and only then is the - * image signed for wolfBoot, so the pins share the image's root of trust. - * An unpatched slot advertises zero records, which fails launch closed. */ +/* Pinned guest-measurement slot: the image patcher stamps the guest digests + * by magic before wolfBoot signs the image, so the pins share its root of + * trust; an unpatched slot advertises zero records and fails launch closed. */ #define WT_GUEST_MEAS_SLOT_MAGIC_LEN 16u #define WT_GUEST_MEAS_SLOT_UNPATCHED 0xFFFFFFFFu -/* Enforcement the MCXN947 port provides (WT-PORT-0008). The fabric filter is - * the per-dispatch SAU window, as on the MIMXRT700: it isolates CPU0's guests - * from each other and says nothing about CPU1, DMA or any other bus master, - * which the TRDC and AHB secure controller are not yet programmed to fence. */ +/* Enforcement the port provides (WT-PORT-0008): the fabric filter is the + * per-dispatch SAU window, as on the MIMXRT700, isolating CPU0's guests only; + * CPU1, DMA and other bus masters are not fenced (no TRDC/AHBSC rules yet). */ #define WT_MCXN947_PORT_CAPABILITIES \ (WT_PORT_CAPABILITY_VECTOR_READ_ALIAS | \ WT_PORT_CAPABILITY_NS_DOMAIN_PROGRAMMING | \ diff --git a/port/mcxn947/platform_mcxn947.c b/port/mcxn947/platform_mcxn947.c index 9b429e65..9a81b546 100644 --- a/port/mcxn947/platform_mcxn947.c +++ b/port/mcxn947/platform_mcxn947.c @@ -51,11 +51,9 @@ static volatile uint32_t g_secure_service_depth; static volatile uint32_t g_hsm_wait_skip_count; -/* The IDAU already marks every bit-28-clear alias Non-secure; the SAU must - * agree or the more secure attribute wins. The guest RAM extent is NOT a - * static Non-secure region: each dispatch marks only the arriving guest's - * window Non-secure through the dynamic regions below, so the peer's window - * stays Secure by SAU default while it runs, as on the MIMXRT700. */ +/* The SAU must agree with the IDAU or the more secure attribute wins. Guest + * RAM is not a static region: each dispatch opens only the arriving guest's + * window (dynamic regions below), so the peer's stays Secure while it runs. */ static const wt_armv8m_sau_region_t g_sau_regions[] = { { WT_GUEST0_FLASH_BASE, WT_GUEST1_FLASH_BASE + WT_GUEST1_FLASH_SIZE - 1u, false }, @@ -163,11 +161,9 @@ void wt_platform_launch_debug(int code, uint32_t guest) } #endif -/* SAU guest-isolation back-end. close_all disables the dynamic regions so the - * whole guest RAM extent falls to the SAU default (Secure); open_window marks - * one guest window Non-secure in the next free dynamic region. A Non-secure - * store into the peer's now-Secure window raises a SecureFault the monitor - * contains. Mirrors the STM32H5 GTZC path through the shared window helper. */ +/* SAU guest-isolation back-end: close_all drops every dynamic region (guest + * RAM falls to the SAU default, Secure), open_window marks one window + * Non-secure, so a store into the peer's window faults and is contained. */ static void wt_sau_close_all(void) { uint32_t rnr; diff --git a/port/mcxn947/secure.ld b/port/mcxn947/secure.ld index 5fb90d5d..3fc30331 100644 --- a/port/mcxn947/secure.ld +++ b/port/mcxn947/secure.ld @@ -35,10 +35,9 @@ SECTIONS { KEEP(*(.vectors)) } > FLASH - /* Linker-synthesized SG veneers land here (--cmse-implib) in the image's - * second 1 KiB, the SAU NSC window (WT_NSC_* in memory_map.h); the veneer - * bodies live in .text. The dot assignment keeps the section address - * assigned even when no input object carries .gnu.sgstubs content. */ + /* Linker-synthesized SG veneers (--cmse-implib) in the image's second + * 1 KiB, the SAU NSC window (WT_NSC_* in memory_map.h); the explicit + * address keeps the section placed even with no .gnu.sgstubs input. */ .gnu.sgstubs (WT_SECURE_FLASH_ORIGIN + WT_SECURE_IMAGE_HEADER_SIZE + 0x400) : { *(.gnu.sgstubs) *(.gnu.sgstubs*) @@ -80,12 +79,9 @@ SECTIONS { "secure image overflows secure flash region") ASSERT(SIZEOF(.gnu.sgstubs) + SIZEOF(.nsc) <= 0x400, "NSC window overflow") - /* Claim the conformance partitions' .data/.bss before the general rules so - * they land in CONFDATA, not shared SPM RAM. Loaded from flash and copied by - * Reset_Handler alongside .data; empty (zero size) in non-conformance builds. - * The server and driver partitions each get a 32-byte-aligned private band - * the SVC gate denies to every other partition; their .bss lands in the - * loaded section so each band stays one contiguous MPU carve. */ + /* Conformance partition .data/.bss, claimed before the general rules so it + * lands in CONFDATA (empty outside conformance builds); the server and + * driver bands stay contiguous MPU carves the gate denies to other SPs. */ .conf_data : { . = ALIGN(4); _sconfdata = .; diff --git a/tests/firmware/mcxn947-baremetal/guest0.c b/tests/firmware/mcxn947-baremetal/guest0.c index 31b4a657..f24b7da0 100644 --- a/tests/firmware/mcxn947-baremetal/guest0.c +++ b/tests/firmware/mcxn947-baremetal/guest0.c @@ -18,12 +18,9 @@ * along with this program; if not, see . */ -/* Bare-metal Non-secure bring-up guest for the MCXN947 CPU0. wolfBoot - * authenticates the wolfTrust Secure image, which launches this guest in the - * Non-secure state; the guest drives the WolfTrust_FFM_* veneers to prove the - * Secure runtime booted and services a Non-secure PSA client. Progress lands - * in a debugger-readable mailbox at the base of guest RAM and, best effort, - * on LPUART4 (the FRDM board's MCU-Link console the first loader set up). */ +/* Bare-metal Non-secure bring-up guest for the MCXN947 CPU0: drives the + * WolfTrust_FFM_* veneers to prove the Secure runtime serves a Non-secure PSA + * client, reporting in a mailbox at the base of guest RAM and on LPUART4. */ #include #include @@ -92,10 +89,9 @@ static void guest0_periph_probe(volatile wt_guest0_mailbox_t* mb) #endif #if defined(WT_SAU_PROBE) -/* Isolation probe: GUEST_PROBE_ADDR is the peer guest's RAM, Secure to the SAU - * while this guest runs. A privileged guest can switch off the Non-secure MPU - * wolfTrust programs for it, so attribution alone must stop the store. Latch: - * 1 attempted (the store faulted), 2 blocked, 3 leaked. */ +/* Isolation probe: GUEST_PROBE_ADDR is the peer's RAM, Secure to the SAU + * while this guest runs; with the guest's own MPU off, attribution alone + * must stop the store. Latch: 1 attempted (faulted), 2 blocked, 3 leaked. */ #define GUEST0_MPU_CTRL (*(volatile uint32_t*)0xE000ED94u) #define GUEST0_PROBE_ATTEMPTED 1u #define GUEST0_PROBE_BLOCKED 2u diff --git a/tests/host/mcxn947_flash/main.c b/tests/host/mcxn947_flash/main.c index 1a312d40..9908bdae 100644 --- a/tests/host/mcxn947_flash/main.c +++ b/tests/host/mcxn947_flash/main.c @@ -18,14 +18,9 @@ * along with this program; if not, see . */ -/* Host suite for the MCXN947 ROM flash driver and the wolfHSM store on top - * of it. The model is the part's ECC flash: 16-byte phrases that bits can - * only be cleared in, and that a second program of (before an erase) leaves - * with an invalid ECC, counted here as a double program. The real driver - * (port/mcxn947/rom_flash.c) runs against a mock of the boot ROM's flash - * API tree; the wolfHSM NVM lifecycle then runs over it with the port's - * 16-byte unit, and, built with the 8-byte default, over a raw backend that - * shows two units sharing a phrase being written at different times. */ +/* Host suite for the MCXN947 ROM flash driver and the wolfHSM store over a + * model of the part's ECC flash (a 16-byte phrase programmed twice between + * erases is counted as a double program); built per wolfHSM unit size. */ #include #include diff --git a/tests/host/mcxn947_lifecycle/main.c b/tests/host/mcxn947_lifecycle/main.c index 5fd432ca..a036c50c 100644 --- a/tests/host/mcxn947_lifecycle/main.c +++ b/tests/host/mcxn947_lifecycle/main.c @@ -19,9 +19,8 @@ */ /* The MCXN947 life cycle mapping wolfBoot hands wolfTrust, tested from the - * copy inside the carried wolfBoot patch: only an agreeing OEM life cycle - * maps to a PSA state, no NXP-internal, blank or shredded state does, and no - * fault or debug state reads as SECURED. */ + * copy in the carried patch: only an agreeing OEM state maps to PSA, and no + * fault or open-debug state reads as SECURED. */ #include "mcxn_lifecycle.h" diff --git a/tests/target/provisioning/provisioning_ctrl_mcxn947.sh b/tests/target/provisioning/provisioning_ctrl_mcxn947.sh index d182fbbc..d8525ed8 100644 --- a/tests/target/provisioning/provisioning_ctrl_mcxn947.sh +++ b/tests/target/provisioning/provisioning_ctrl_mcxn947.sh @@ -19,10 +19,8 @@ # along with this program; if not, see . # MCXN947 (FRDM-MCXN947) port for provisioning_ctrl.sh: the OTP life cycle -# the ROM reflects into SYSCON, rehearsed by handing wolfTrust the target -# state's PSA life cycle in the boot handoff record (a reset undoes it). -# No permanent write is implemented: every lock is refused. Sourced, never -# run. See docs/MCXN947-Guide.md. +# the ROM reflects into SYSCON, rehearsed in the boot handoff record; no +# permanent write is implemented. Sourced, never run. See docs/MCXN947-Guide.md. # shellcheck disable=SC2034,SC2154 # PORT_* are read, and $cur/$PENDING set, by the main script # A port file only works inside provisioning_ctrl.sh, which holds the gates. From a8c54fc52991534f1fec25c697d61d3f883f5422 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 23 Oct 2026 22:08:06 +0100 Subject: [PATCH 09/45] Claim only the MCXN947 scenarios run on M33MU --- tests/target/lib/scenario_matrix.py | 42 ++++++----------------------- 1 file changed, 8 insertions(+), 34 deletions(-) diff --git a/tests/target/lib/scenario_matrix.py b/tests/target/lib/scenario_matrix.py index f5feb48b..afe2fae2 100755 --- a/tests/target/lib/scenario_matrix.py +++ b/tests/target/lib/scenario_matrix.py @@ -162,44 +162,18 @@ "label": "ci:mcxn947", "arm": "mcxn947-m33mu", "image": "ghcr.io/wolfssl/wolfboot-ci-m33mu:v1.25", - "smoke": ("positive", "sauneg", "crossdomain", "bothpsa", "confboot", - "devcrypto"), + "smoke": ("positive", "sauneg", "crossdomain"), "groups": ( ("positive", "MCXN947 positive lifecycle (SAU guest windows)"), ("sauneg", "MCXN947 cross-guest store faults and is contained"), - ("restart authneg", "MCXN947 guest restart budget and launch refusal"), - ("rollbackneg remeasureneg manifestneg spbudgetneg", - "MCXN947 secure verdict negatives"), - ("crossdomain keystoreneg", "MCXN947 SP domain isolation negatives"), - ("spfaultneg panicneg", "MCXN947 SP fault and panic recovery"), - ("fpneg", "MCXN947 FP isolation (partition FP faults, contained)"), - ("sealbootneg", "MCXN947 damaged main-stack seal refuses to boot"), - ("bothpsa bothiso", "MCXN947 both-guest PSA lifecycle and isolation"), - ("attestneg fwustage", - "MCXN947 attestation negatives and FWU staging"), - ("hsmattackneg", - "MCXN947 wolfHSM cross-namespace + NVM relay negatives"), - ("confboot", "MCXN947 FF-M IPC conformance (85/4)"), - ("devstorage devattest devattestqcbor", - "MCXN947 dev_apis storage and attestation conformance"), - ("devcrypto vaultrecover vaultrecoversec", - "MCXN947 dev_apis crypto conformance and vault recovery"), - ("periphneg", "MCXN947 NS read of the SPM's ELS refused by the SAU"), - ("deputyneg", "MCXN947 keystore-flash privileged deputy refused (L3)"), - ("hsmpinneg hsmfaultneg", - "MCXN947 wolfHSM pointer pin and tasklet fault containment (L3)"), - ("bandneg1 bandneg2 bandneg3 bandneg4 bandneg5 bandneg6", - "MCXN947 partition band-to-band isolation negatives (L3)"), - ("restartneg1 restartneg2 restartneg3", - "MCXN947 partitions restart on a reset band (L3)"), - ("periphspneg manifestneg2 manifestneg3", - "MCXN947 SPM peripheral and manifest level negatives (L3)"), - ("sealneg sealhaltneg sealpivotneg", - "MCXN947 stack seal containment and halt (L3)"), - ("mspovfneg xnneg svcneg", - "MCXN947 SPM stack, execute-never and SVC misuse (L3)"), + ("crossdomain", "MCXN947 SP domain isolation negative (L3)"), ), - "l3_exempt": {}, + # Port bring-up: only scenarios run on the MCXN947 M33MU model are + # listed; each level 3 scenario leaves this map the day it is run. + "l3_exempt": { + s: "not yet run on the MCXN947 M33MU model" + for s in sorted(L3_REQUIRED) if s != "crossdomain" + }, }, } From 3e9015b9a4fdbf91116278113623727d21bbbe16 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Sat, 24 Oct 2026 03:38:46 +0100 Subject: [PATCH 10/45] Program MCXN947 flash from a page base as the ROM requires --- port/mcxn947/memory_map.h | 6 ++-- port/mcxn947/platform_mcxn947.c | 8 +++-- port/mcxn947/rom_flash.c | 31 +++++++++++++++-- tests/host/mcxn947_flash/main.c | 60 ++++++++++++++++++++++++++++---- tests/target/m33mu-mcxn947.patch | 56 +++++++++++++++++++++++++++++ 5 files changed, 148 insertions(+), 13 deletions(-) diff --git a/port/mcxn947/memory_map.h b/port/mcxn947/memory_map.h index 0a280c12..4c011e18 100644 --- a/port/mcxn947/memory_map.h +++ b/port/mcxn947/memory_map.h @@ -60,9 +60,11 @@ #endif /* Flash geometry: 8 KiB erase sectors, 16-byte ECC phrases that are written - * once per erase. Every store below the Secure image lives in bank 1, so the - * image keeps executing from bank 0 while the ROM API programs them. */ + * once per erase, 128-byte pages the ROM API programs from. Every store below + * the Secure image lives in bank 1, so the image keeps executing from bank 0 + * while the ROM API programs them. */ #define WT_FLASH_SECTOR_SIZE 0x00002000u +#define WT_FLASH_PAGE_SIZE 0x00000080u #define WT_FLASH_PHRASE_SIZE 0x00000010u #define WT_FLASH_BANK_SIZE 0x00100000u #define WT_HSM_NVM_FLASH_BASE_NS 0x001E0000u diff --git a/port/mcxn947/platform_mcxn947.c b/port/mcxn947/platform_mcxn947.c index 9a81b546..be378b06 100644 --- a/port/mcxn947/platform_mcxn947.c +++ b/port/mcxn947/platform_mcxn947.c @@ -100,10 +100,12 @@ static const wt_armv8m_mpu_region_t g_mpu_s_whitelist[] = { WT_MPU_RBAR_XN | WT_MPU_RBAR_AP_RW | WT_MPU_RBAR_SH_INNER, WT_MPU_RLAR_ATTRIDX_NORMAL }, - /* Region 4: flash NS alias, read-only XN for guest image metadata. */ + /* Region 4: flash NS alias, XN. Writable because the ROM flash driver + * programs a phrase by storing it through this alias for the FMU to + * capture; outside a ROM program sequence a store cannot alter flash. */ { WT_FLASH_NS_BASE, WT_FLASH_NS_BASE + WT_FLASH_SIZE - 1u, - WT_MPU_RBAR_XN | WT_MPU_RBAR_AP_RO | WT_MPU_RBAR_SH_INNER, - WT_MPU_RLAR_ATTRIDX_NORMAL }, + WT_MPU_RBAR_XN | WT_MPU_RBAR_AP_RW | WT_MPU_RBAR_SH_INNER, + WT_MPU_RLAR_ATTRIDX_NOCACHE }, /* Region 5: peripheral aperture, both aliases (SYSCON, FMU, ELS, * LPUART, TRDC, AHBSC). */ diff --git a/port/mcxn947/rom_flash.c b/port/mcxn947/rom_flash.c index df5befb2..5214063a 100644 --- a/port/mcxn947/rom_flash.c +++ b/port/mcxn947/rom_flash.c @@ -197,9 +197,20 @@ int wt_rom_flash_blank(uint32_t offset, uint32_t size) return WT_ROM_FLASH_OK; } +/* The ROM programs only from a page base (status 101 on silicon for any other + * start) and an all-ones phrase changes nothing, so a phrase is programmed + * from its page base behind all-ones padding, one page per ROM call. */ int wt_rom_flash_program(uint32_t offset, const uint8_t *data, uint32_t size) { + uint8_t page[WT_FLASH_PAGE_SIZE] + __attribute__((aligned(WT_FLASH_PHRASE_SIZE))); + volatile uint8_t *wipe = page; uint32_t primask; + uint32_t done = 0u; + uint32_t base; + uint32_t head; + uint32_t chunk; + uint32_t i; int ret; if (data == NULL || @@ -210,12 +221,28 @@ int wt_rom_flash_program(uint32_t offset, const uint8_t *data, uint32_t size) if (ret == WT_ROM_FLASH_OK) { ret = wt_rom_flash_blank(offset, size); } - if (ret == WT_ROM_FLASH_OK) { + while (ret == WT_ROM_FLASH_OK && done < size) { + base = (offset + done) & ~(WT_FLASH_PAGE_SIZE - 1u); + head = (offset + done) - base; + chunk = WT_FLASH_PAGE_SIZE - head; + if (chunk > size - done) { + chunk = size - done; + } + for (i = 0u; i < head; i++) { + page[i] = 0xFFu; + } + for (i = 0u; i < chunk; i++) { + page[head + i] = data[done + i]; + } primask = wt_rom_flash_irq_mask(); ret = wt_rom_flash_status(wt_rom_flash_api()->program( - g_rom_flash_config, offset, (uint8_t *)(uintptr_t)data, size)); + g_rom_flash_config, base, page, head + chunk)); wt_rom_flash_cache_clear(); wt_rom_flash_irq_restore(primask); + done += chunk; + } + for (i = 0u; i < WT_FLASH_PAGE_SIZE; i++) { + wipe[i] = 0u; } return ret; } diff --git a/tests/host/mcxn947_flash/main.c b/tests/host/mcxn947_flash/main.c index 9908bdae..23988ad7 100644 --- a/tests/host/mcxn947_flash/main.c +++ b/tests/host/mcxn947_flash/main.c @@ -19,8 +19,8 @@ */ /* Host suite for the MCXN947 ROM flash driver and the wolfHSM store over a - * model of the part's ECC flash (a 16-byte phrase programmed twice between - * erases is counted as a double program); built per wolfHSM unit size. */ + * model of the ECC flash and its ROM (page-based programs, all-ones phrases + * untouched, a phrase programmed twice between erases counted); per unit. */ #include #include @@ -59,6 +59,7 @@ typedef struct mock_api_tree { } mock_api_tree_t; #define PHRASE WT_FLASH_PHRASE_SIZE +#define PAGE WT_FLASH_PAGE_SIZE #define SECTOR WT_FLASH_SECTOR_SIZE #define PHRASES (WT_FLASH_SIZE / PHRASE) #define ERASE_KEY 0x6B65666Cu @@ -76,6 +77,7 @@ static uint8_t g_programmed[PHRASES]; static uint32_t g_double_programs; static uint32_t g_first_double_offset; static uint32_t g_program_calls; +static uint32_t g_unaligned_programs; static uint32_t g_erase_calls; static uint32_t g_verify_erase_calls; static uint32_t g_init_calls; @@ -99,16 +101,31 @@ static void check(int cond, const char *name) } } -/* Program as the array does: bits clear, and a phrase written twice since - * its erase no longer carries a valid ECC. */ +/* Program as the array does: bits clear, an all-ones phrase changes nothing, + * and a phrase written twice since its erase no longer carries a valid ECC. */ static void model_program(uint32_t start, const uint8_t *src, uint32_t len) { uint32_t first = start / PHRASE; uint32_t last = (start + len - 1u) / PHRASE; + uint32_t lo; + uint32_t hi; uint32_t p; uint32_t i; + int touched; for (p = first; p <= last; p++) { + lo = (p * PHRASE > start) ? p * PHRASE : start; + hi = ((p + 1u) * PHRASE < start + len) ? (p + 1u) * PHRASE : + start + len; + touched = 0; + for (i = lo; i < hi; i++) { + if (src[i - start] != 0xFFu) { + touched = 1; + } + } + if (touched == 0) { + continue; + } if (g_programmed[p] != 0u) { if (g_double_programs == 0u) { g_first_double_offset = p * PHRASE; @@ -177,8 +194,12 @@ static int32_t mock_program(void *config, uint32_t start, uint8_t *src, { (void)config; g_program_calls++; - if ((start % 4u) != 0u || (length % 4u) != 0u || - start + length > WT_FLASH_SIZE) { + /* As on silicon: only a page base is accepted as the start. */ + if ((start % PAGE) != 0u) { + g_unaligned_programs++; + return STATUS_ALIGN; + } + if ((length % 4u) != 0u || start + length > WT_FLASH_SIZE) { return STATUS_ALIGN; } if (g_program_status != STATUS_OK) { @@ -316,6 +337,31 @@ static void test_driver(void) wt_rom_flash_last_status() == 105u, "a ROM erase failure is reported with its status"); g_erase_status = STATUS_OK; + + check(wt_rom_flash_erase(NVM_OFF, SECTOR) == WT_ROM_FLASH_OK, + "the sector erases before the page checks"); + check(g_mock_api.program(NULL, NVM_OFF + PHRASE, phrase, PHRASE) == + STATUS_ALIGN, "the model refuses a start off a page base"); + g_program_calls = 0u; + g_unaligned_programs = 0u; + check(wt_rom_flash_program(NVM_OFF + 3u * PHRASE, phrase, PHRASE) == + WT_ROM_FLASH_OK && g_program_calls == 1u && + g_unaligned_programs == 0u, + "a lone phrase is programmed by one page-based ROM call"); + check(wt_rom_flash_blank(NVM_OFF, 3u * PHRASE) == WT_ROM_FLASH_OK && + memcmp(g_flash + NVM_OFF + 3u * PHRASE, phrase, PHRASE) == 0, + "the all-ones padding left the phrases before it blank"); + check(wt_rom_flash_program(NVM_OFF + 4u * PHRASE, two, PHRASE) == + WT_ROM_FLASH_OK && g_double_programs == 0u && + memcmp(g_flash + NVM_OFF + 3u * PHRASE, phrase, PHRASE) == 0 && + memcmp(g_flash + NVM_OFF + 4u * PHRASE, two, PHRASE) == 0, + "padding over a programmed phrase is not a program of it"); + g_program_calls = 0u; + check(wt_rom_flash_program(NVM_OFF + PAGE - PHRASE, two, sizeof(two)) == + WT_ROM_FLASH_OK && g_program_calls == 2u && + g_unaligned_programs == 0u && + memcmp(g_flash + NVM_OFF + PAGE - PHRASE, two, sizeof(two)) == 0, + "a program crossing a page boundary takes one ROM call per page"); } /* The port backend's rules (port/mcxn947/hsm_flash.c) over the real driver: @@ -618,6 +664,8 @@ static void test_store(void) (unsigned)g_double_programs); check(g_double_programs == 0u, "16-byte units: no phrase was programmed twice between erases"); + check(g_unaligned_programs == 0u, + "16-byte units: every ROM program started on a page base"); /* The unit, not the port's guard, is what keeps every program whole: * a backend with no phrase rule stays clean at 16 bytes. */ model_reset(); diff --git a/tests/target/m33mu-mcxn947.patch b/tests/target/m33mu-mcxn947.patch index 5c1c1483..ae8d96a9 100644 --- a/tests/target/m33mu-mcxn947.patch +++ b/tests/target/m33mu-mcxn947.patch @@ -22,6 +22,62 @@ index 831de76..875d3cf 100644 scg.regs[SCG_VERID / 4u] = 0x00000000u; scg.regs[SCG_PARAM / 4u] = 0x000001FEu; +diff --git a/cpu/mcxn947/mcxn947_romapi.c b/cpu/mcxn947/mcxn947_romapi.c +index e98f570..3ac8b98 100644 +--- a/cpu/mcxn947/mcxn947_romapi.c ++++ b/cpu/mcxn947/mcxn947_romapi.c +@@ -112,8 +112,11 @@ + + #define ROMAPI_FLASH_ERASE_KEY 0x6b65666cu + ++#define ROMAPI_FLASH_PAGE_SIZE 128u ++ + #define ROMAPI_STATUS_FLASH_SUCCESS 0u + #define ROMAPI_STATUS_FLASH_INVALID_ARGUMENT 4u ++#define ROMAPI_STATUS_FLASH_ALIGNMENT_ERROR 101u + #define ROMAPI_STATUS_FLASH_ADDRESS_ERROR 102u + #define ROMAPI_STATUS_FLASH_ACCESS_ERROR 103u + #define ROMAPI_STATUS_FLASH_ERASE_KEY_ERROR 107u +@@ -320,6 +323,23 @@ static mm_bool romapi_memcpy_from_map(const struct mm_memmap *map, enum mm_sec_s + return MM_TRUE; + } + ++/* Programming only clears bits, so an all-ones phrase leaves the array as it ++ * is; that is how a phrase gets programmed from its page base on silicon. */ ++static mm_bool romapi_flash_program_from_map(const struct mm_memmap *map, enum mm_sec_state sec, ++ mm_u32 dst, mm_u32 src, mm_u32 len, struct mm_memmap *wmap) ++{ ++ mm_u32 i; ++ mm_u8 b; ++ mm_u8 old; ++ if (map == 0 || wmap == 0) return MM_FALSE; ++ for (i = 0; i < len; ++i) { ++ if (!romapi_read_mem8(map, sec, src + i, &b)) return MM_FALSE; ++ if (!romapi_read_mem8(map, sec, dst + i, &old)) return MM_FALSE; ++ if (!romapi_write_mem8(wmap, sec, dst + i, (mm_u8)(old & b))) return MM_FALSE; ++ } ++ return MM_TRUE; ++} ++ + static mm_bool romapi_memset_map(struct mm_memmap *map, enum mm_sec_state sec, mm_u32 dst, mm_u8 value, mm_u32 len) + { + mm_u32 i; +@@ -493,10 +513,14 @@ static mm_u32 romapi_flash_program(struct mm_cpu *cpu, struct mm_memmap *map) + src = cpu->r[2]; + len = cpu->r[3]; + if (len == 0u) return ROMAPI_STATUS_FLASH_SUCCESS; ++ /* The ROM takes a program only from a page base (status 101 on silicon). */ ++ if ((start % ROMAPI_FLASH_PAGE_SIZE) != 0u || (len % 4u) != 0u) { ++ return ROMAPI_STATUS_FLASH_ALIGNMENT_ERROR; ++ } + if (!romapi_flash_offset(map, start, len, &flash_off)) { + return ROMAPI_STATUS_FLASH_ADDRESS_ERROR; + } +- if (!romapi_memcpy_from_map(map, cpu->sec_state, start, src, len, map)) { ++ if (!romapi_flash_program_from_map(map, cpu->sec_state, start, src, len, map)) { + return ROMAPI_STATUS_FLASH_ACCESS_ERROR; + } + romapi_persist_flush(flash_off, len); diff --git a/cpu/mcxn947/mcxn947_secure.c b/cpu/mcxn947/mcxn947_secure.c index 148ef22..3f1941c 100644 --- a/cpu/mcxn947/mcxn947_secure.c From 7da59db5298792fd0faf1f30c1aa4d15f271852a Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Sat, 24 Oct 2026 03:38:46 +0100 Subject: [PATCH 11/45] Let the MCXN947 guest build name the Secure image build tree --- tests/firmware/mcxn947-baremetal/Makefile | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/firmware/mcxn947-baremetal/Makefile b/tests/firmware/mcxn947-baremetal/Makefile index fbee5bf0..6b14e83e 100644 --- a/tests/firmware/mcxn947-baremetal/Makefile +++ b/tests/firmware/mcxn947-baremetal/Makefile @@ -13,7 +13,11 @@ TARGET ?= mcxn947 ARCH ?= armv8m BUILD ?= build -TOP_BUILD_DIR := $(ROOT)/build +# The Secure image's build directory, relative to the repository root; the +# hardware runner points both at its own tree so an emulator run can share +# the checkout. +WT_BUILD_DIR ?= build +TOP_BUILD_DIR := $(ROOT)/$(WT_BUILD_DIR) SECURE_CMSE_IMPLIB := $(TOP_BUILD_DIR)/secure_cmse_implib.o # The same guest links into both Non-secure windows (port/mcxn947/memory_map.h); @@ -63,7 +67,7 @@ all: $(BUILD)/guest0.bin $(BUILD)/guest1.bin # relink the image in another mode and drop the wolftrust.bin the runners sign. $(SECURE_CMSE_IMPLIB): FORCE $(MAKE) -C $(ROOT) ARCH=$(ARCH) TARGET=$(TARGET) TOOLPREFIX=$(TOOLPREFIX) \ - build/secure_cmse_implib.o + BUILD_DIR=$(WT_BUILD_DIR) $(WT_BUILD_DIR)/secure_cmse_implib.o $(BUILD)/guest0.o: guest0.c | $(BUILD) $(CC) $(CFLAGS) $(GUEST0_CFLAGS) -c -o $@ $< From b6fc4a5c00d068f33db57d9074a2ad7e790cbbc1 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Sat, 24 Oct 2026 04:32:37 +0100 Subject: [PATCH 12/45] Add the FRDM-MCXN947 hardware runner --- tests/target/detect_mcxn947.sh | 38 ++- tests/target/run_mcxn947_hardware.sh | 422 +++++++++++++++++++++++++++ 2 files changed, 449 insertions(+), 11 deletions(-) create mode 100755 tests/target/run_mcxn947_hardware.sh diff --git a/tests/target/detect_mcxn947.sh b/tests/target/detect_mcxn947.sh index 93447506..4e022b0b 100755 --- a/tests/target/detect_mcxn947.sh +++ b/tests/target/detect_mcxn947.sh @@ -1,23 +1,39 @@ #!/usr/bin/env bash # Board-present check for the FRDM-MCXN947 rig: the on-board MCU-Link probe -# (NXP 1fc9:0143), its VCOM, a pyOCD with the MCXN947 pack, and SPSDK's blhost -# for the ISP life-cycle path. Exit 0 when usable; otherwise print one SKIP -# reason and exit 1. +# (the one pyOCD lists as "MCU-LINK FRDM-MCXN947", or MCXN947_PROBE_UID), its +# VCOM, a pyOCD with the MCXN947 pack, and SPSDK's blhost for the ISP +# life-cycle path. Exit 0 when usable; otherwise print one SKIP reason and +# exit 1. set -u -MCXN947_SERIAL="${MCXN947_SERIAL:-/dev/ttyACM0}" MCXN947_PYOCD="${MCXN947_PYOCD:-pyocd}" +MCXN947_SPSDK_VENV="${MCXN947_SPSDK_VENV:-$HOME/spsdk-venv}" -if ! command -v lsusb >/dev/null 2>&1 || ! lsusb 2>/dev/null | grep -q "1fc9:0143"; then - echo "MCU-Link probe (1fc9:0143) not attached" - exit 1 +if ! command -v "$MCXN947_PYOCD" >/dev/null 2>&1; then + if [ -x "$MCXN947_SPSDK_VENV/bin/pyocd" ]; then + PATH="$MCXN947_SPSDK_VENV/bin:$PATH" + export PATH + else + echo "pyocd not installed" + exit 1 + fi +fi +probes="$("$MCXN947_PYOCD" list 2>/dev/null | grep -F "FRDM-MCXN947" || true)" +if [ -n "${MCXN947_PROBE_UID:-}" ]; then + probes="$("$MCXN947_PYOCD" list 2>/dev/null | grep -F "$MCXN947_PROBE_UID" || true)" fi -if [ ! -c "$MCXN947_SERIAL" ]; then - echo "board console $MCXN947_SERIAL not present" +if [ -z "$probes" ]; then + echo "MCU-Link FRDM-MCXN947 probe not attached (set MCXN947_PROBE_UID for another)" exit 1 fi -if ! command -v "$MCXN947_PYOCD" >/dev/null 2>&1; then - echo "pyocd not installed" +console="${MCXN947_CONSOLE:-}" +if [ -z "$console" ]; then + for tty in /dev/serial/by-id/*FRDM-MCXN947*-if02; do + [ -c "$tty" ] && console="$tty" && break + done +fi +if [ -z "$console" ] || [ ! -c "$console" ]; then + echo "board console not present (MCXN947_CONSOLE)" exit 1 fi if ! "$MCXN947_PYOCD" pack find mcxn947 2>/dev/null | grep -q "True"; then diff --git a/tests/target/run_mcxn947_hardware.sh b/tests/target/run_mcxn947_hardware.sh new file mode 100755 index 00000000..a73f8587 --- /dev/null +++ b/tests/target/run_mcxn947_hardware.sh @@ -0,0 +1,422 @@ +#!/usr/bin/env bash +# FRDM-MCXN947 hardware runner (sibling of run_rt700_hardware.sh and +# run_mcxn947_m33mu.sh; one CLI shape). Runs on the host that owns the probe. +# +# run_mcxn947_hardware.sh +# +# Scenarios: +# positive the full chain: wolfBoot (TrustZone loader) authenticates the +# wolfTrust Secure image, which launches both Non-secure guests. +# Builds wolfTrust + the guests, pins the guest measurements, +# wolfBoot-signs the Secure image, flashes the chain, erases the +# update, swap and NVM sectors so the run starts from a fresh +# vault, resets, verifies every image by readback, and asserts the +# guest mailboxes, the launch masks, the ROM flash status and the +# vault's first phrase over SWD. +# sauneg positive plus the guest isolation negative: guest0 stores into +# guest1's RAM, which the per-dispatch SAU window keeps Secure; the +# store must fault on every launch until guest0 is quarantined, +# guest1's RAM never holds the sentinel, and guest1 keeps running. +# periphneg guest0, its MPU off, reads the SPM's ELS through the Non-secure +# alias on every launch; the SAU must refuse it (no leak) until +# guest0 is quarantined, and guest1 keeps running. +# restart guest0 reads Secure RAM on every launch: the monitor spends its +# restart budget and quarantines it; guest1 is untouched. +# psa both Non-secure windows run the portable PSA client +# (tests/firmware/psa-guest): every lifecycle stage must complete +# in both mailboxes, with the ELS behind psa_generate_random. +# +# The probe is the board's MCU-Link, the one pyOCD lists as +# "MCU-LINK FRDM-MCXN947" (MCXN947_PROBE_UID picks one of several); the +# console is its VCOM (MCXN947_CONSOLE). The wolfBoot first stage is +# MCXN947_WOLFBOOT_REF plus the carried patch (lib/mcxn947_wolfboot.sh) +# unless MCXN947_WOLFBOOT_DIR names a tree that library built. +set -euo pipefail + +scenario="${1:-}" +here="$(cd "$(dirname "$0")" && pwd)" +repo="$(cd "$here/../.." && pwd)" +work="${MCXN947_WORK:-$repo/build/mcxn947-hw}" +target="${MCXN947_TARGET:-mcxn947vdf}" +probe_uid="${MCXN947_PROBE_UID:-}" +console="${MCXN947_CONSOLE:-}" +wolfboot_dir="${MCXN947_WOLFBOOT_DIR:-}" +spsdk_venv="${MCXN947_SPSDK_VENV:-$HOME/spsdk-venv}" +console_seconds="${MCXN947_CONSOLE_SECONDS:-25}" +# Physical flash offsets (port/mcxn947/memory_map.h, wolfBoot's +# mcxn-tz-wolftrust config): the debugger and the ROM see the array at 0. +wolfboot_flash_addr=0x00000000 +secure_flash_addr=0x00040000 +guest0_flash_addr=0x00080000 +guest1_flash_addr=0x00100000 +update_erase=0x00180000-0x001C2000 +nvm_erase=0x001E0000-0x001E6000 +hsm_nvm_addr=0x001E0000 +flash_sector=0x2000 +guest0_ram=0x20000000 +guest1_ram=0x20018000 +probe_addr=0x20028000 +handoff_addr=0x30030000 +syscon_lc_state=0x50000980 +# The guests' manifest restart budget (port/mcxn947/partitions.c). +restart_limit=3 +baremetal_dir="$repo/tests/firmware/mcxn947-baremetal" +psa_dir="$repo/tests/firmware/psa-guest" + +# shellcheck source=lib/mcxn947_wolfboot.sh disable=SC1091 +. "$here/lib/mcxn947_wolfboot.sh" + +log() { printf '%s\n' "$*"; } +stage() { printf ' ... %s\n' "$*"; } +fail() { log "FAIL: $*"; exit 1; } +check() { + if [ "$1" -eq 0 ]; then log " [check] PASS $2"; else log " [check] FAIL $2"; fail "$2"; fi +} + +ensure_spsdk() { + if ! command -v pyocd >/dev/null 2>&1; then + [ -x "$spsdk_venv/bin/pyocd" ] || fail "pyocd not found (set MCXN947_SPSDK_VENV)" + PATH="$spsdk_venv/bin:$PATH" + export PATH + fi +} + +# The board's own MCU-Link, unless the caller named a probe; other probes on +# the host belong to other boards and are never touched. +ensure_probe() { + local listed + if [ -z "$probe_uid" ]; then + listed="$(pyocd list 2>/dev/null | grep -F "MCU-LINK FRDM-MCXN947" || true)" + [ "$(printf '%s\n' "$listed" | grep -c .)" -eq 1 ] || \ + fail "expected one MCU-Link FRDM-MCXN947 probe (set MCXN947_PROBE_UID)" + probe_uid="$(printf '%s\n' "$listed" | awk '{ for (i = 1; i <= NF; i++) if ($i ~ /^[A-Z0-9]{8,}$/) { print $i; exit } }')" + [ -n "$probe_uid" ] || fail "could not read the probe's unique id" + fi + if [ -z "$console" ]; then + for tty in /dev/serial/by-id/*FRDM-MCXN947*-if02; do + [ -c "$tty" ] && console="$tty" && break + done + fi + [ -n "$console" ] && [ -c "$console" ] || fail "board console not found (set MCXN947_CONSOLE)" +} + +# Observation uses the generic Cortex-M attach: the device-pack target resets +# the chip on connect, so a read would land in a fresh boot, not the one under +# test. +dap() { + timeout 60 pyocd cmd -u "$probe_uid" -t cortex_m "$@" 2>&1 | + grep -viE "rom table|APB-AP|coresight|cidr" +} + +# Park the core before wolfTrust owns it: pyOCD's flash algorithm runs in RAM +# and faults under wolfTrust's Secure MPU whitelist, so the halt is followed +# by disabling the MPU explicitly. +park_core() { + timeout 60 pyocd cmd -u "$probe_uid" -t "$target" -O resume_on_disconnect=false \ + -O reset_type=hw -c "reset halt" -c "write32 0xE000ED94 0" \ + >/dev/null 2>&1 || fail "could not park the core" +} + +# A failed or partial flash must stop the run: a stale image either boots old +# code or fails its pinned measurement and looks like a port bug. +flash_at() { + local addr="$1" image="$2" out + [ -s "$image" ] || fail "flash image missing: $image" + park_core + if ! out="$(timeout 300 pyocd flash -u "$probe_uid" -t "$target" \ + -O resume_on_disconnect=false --no-reset -a "$addr" -e sector "$image" 2>&1)"; then + printf '%s\n' "$out" | tail -6 + fail "flash of $(basename "$image") at $addr failed" + fi +} + +# The board keeps the update, swap and NVM sectors across runs where the +# emulator starts fresh; erasing them puts every run on a blank vault. +erase_ranges() { + park_core + timeout 180 pyocd erase -u "$probe_uid" -t "$target" -O resume_on_disconnect=false \ + -s "$@" >/dev/null 2>&1 || fail "erase of $* failed" +} + +# SRAM survives a warm reset, so the last run's mailboxes, sentinel and launch +# masks would otherwise read back as this run's result. +clear_mailboxes() { + local verified refused + verified="$(elf_sym g_wt_launch_verified_mask)" + refused="$(elf_sym g_wt_launch_refused_mask)" + [ -n "$verified" ] && [ -n "$refused" ] || fail "launch masks not found in wolftrust.elf" + park_core + timeout 60 pyocd cmd -u "$probe_uid" -t "$target" -O resume_on_disconnect=false \ + -c "write32 $guest0_ram 0 0 0 0 0 0 0 0 0 0 0 0" \ + -c "write32 $guest1_ram 0 0 0 0 0 0 0 0 0 0 0 0" \ + -c "write32 $probe_addr 0" \ + -c "write32 $verified 0" -c "write32 $refused 0" >/dev/null 2>&1 || \ + fail "could not clear the guest mailboxes" +} + +# Boot from the parked state: the pack's halt leaves a DWT comparator and the +# debug fault status armed, which would stop the chain again, so both are +# cleared before the core is released. The console is read for the whole boot; +# the MCU-Link forwards early-boot UART only while the debug link stays active, +# so the capture is kept company by spaced reads. +release_core() { + local n + stty -F "$console" 115200 raw -echo -crtscts -ixon 2>/dev/null || true + ( timeout "$console_seconds" cat "$console" > "$work/console.log" 2>/dev/null ) & + sleep 0.5 + timeout 60 pyocd cmd -u "$probe_uid" -t "$target" -O resume_on_disconnect=false \ + -O reset_type=hw -c "reset halt" -c "write32 0xE0001028 0" \ + -c "write32 0xE0001038 0" -c "write32 0xE000ED30 0x1F" -c "go" \ + >/dev/null 2>&1 || fail "could not release the core" + for n in 1 2 3 4 5 6; do + sleep 1.5 + dap -c "read32 $guest0_ram" >/dev/null 2>&1 || true + done + wait +} + +# Read an image back over the debug port once the chain has booted and +# compare it. +verify_at() { + local addr="$1" image="$2" size + size="$(wc -c < "$image" | tr -d ' ')" + timeout 120 pyocd cmd -u "$probe_uid" -t cortex_m \ + -c "savemem $addr $size $work/readback.bin" >/dev/null 2>&1 || \ + fail "readback of $addr failed" + cmp -s "$work/readback.bin" "$image" || \ + fail "flash verify mismatch at $addr ($(basename "$image"))" + log " [flash] verified $(basename "$image") @ $addr" +} + +ensure_wolfboot() { + if [ -n "$wolfboot_dir" ]; then + mcxn947_wolfboot_current "$wolfboot_dir" "$(mcxn947_wolfboot_stamp)" || \ + fail "MCXN947_WOLFBOOT_DIR=$wolfboot_dir was not built by lib/mcxn947_wolfboot.sh at $MCXN947_WOLFBOOT_REF" + else + wolfboot_dir="$HOME/.cache/wolftrust/wolfboot-mcxn947" + mkdir -p "$(dirname "$wolfboot_dir")" + stage "wolfBoot $MCXN947_WOLFBOOT_REF (mcxn-tz-wolftrust)" + mcxn947_wolfboot_build "$wolfboot_dir" > "$work/wolfboot-build.log" 2>&1 || { + tail -20 "$work/wolfboot-build.log" + fail "wolfBoot build failed" + } + fi + [ -s "$wolfboot_dir/wolfboot.bin" ] || \ + fail "wolfBoot image missing at $wolfboot_dir/wolfboot.bin (MCXN947_WOLFBOOT_DIR)" +} + +# Build wolfTrust and both guests, pin the guest measurements, sign, flash the +# whole chain, boot it from a fresh vault, and verify every image by readback. +run_chain() { + local guest_dir="$1" guest_flags="$2" guest_build + + ensure_spsdk + ensure_probe + mkdir -p "$work" + ensure_wolfboot + + # MCXN wolfBoot uses a 1024-byte image header, so wolfTrust links at the + # boot base + 0x400 and is signed with a matching header. Exported so the + # secure image and the guest CMSE import library agree. + export WT_SECURE_IMAGE_HEADER_SIZE=0x400 + rm -rf "$repo/build" + mkdir -p "$work" + + stage "build wolfTrust secure image + CMSE import library (${WT_ENGINE:-native})" + make -s -C "$repo" TARGET=mcxn947 secure-image TOOLPREFIX=arm-none-eabi- + + stage "build the Non-secure guests from ${guest_dir#"$repo/"} ${guest_flags:-(no probes)}" + make -s -C "$guest_dir" clean + # The board is OEM_OPEN, the ASSEMBLY_AND_TEST life cycle the attestation + # token reports; the PSA guest's verify pins it. + # shellcheck disable=SC2086 + make -s -C "$guest_dir" TARGET=mcxn947 WT_EXPECTED_LIFECYCLE=0x1000u $guest_flags + guest_build="$guest_dir/build" + + stage "pin both guest measurements, then wolfBoot-sign wolfTrust" + python3 "$repo/tools/measure/patch_guest_digests.py" "$repo/build/wolftrust.bin" \ + "0:1:$guest_build/guest0.bin" "1:1:$guest_build/guest1.bin" + IMAGE_HEADER_SIZE=1024 WOLFBOOT_PARTITION_SIZE=0x40000 WOLFBOOT_SECTOR_SIZE=$flash_sector \ + "$wolfboot_dir/tools/keytools/sign" --ecc256 --sha256 \ + "$repo/build/wolftrust.bin" \ + "$wolfboot_dir/wolfboot_signing_private_key.der" 1 + + stage "flash the chain" + flash_at "$wolfboot_flash_addr" "$wolfboot_dir/wolfboot.bin" + flash_at "$secure_flash_addr" "$repo/build/wolftrust_v1_signed.bin" + flash_at "$guest0_flash_addr" "$guest_build/guest0.bin" + flash_at "$guest1_flash_addr" "$guest_build/guest1.bin" + stage "erase the update, swap and NVM sectors so the run starts from a fresh vault" + erase_ranges "$update_erase" "$nvm_erase" + clear_mailboxes + + stage "boot the chain, reading the console for ${console_seconds}s" + release_core + verify_at "$wolfboot_flash_addr" "$wolfboot_dir/wolfboot.bin" + verify_at "$secure_flash_addr" "$repo/build/wolftrust_v1_signed.bin" + verify_at "$guest0_flash_addr" "$guest_build/guest0.bin" + verify_at "$guest1_flash_addr" "$guest_build/guest1.bin" + log " [console] $work/console.log" +} + +# One 32-bit word at base+offset over SWD, as eight lowercase hex digits. +word_at() { + dap -c "read32 $(printf '0x%x' $(($1 + $2)))" | grep -oiE '[0-9a-f]{8}' | tail -1 | + tr 'A-F' 'a-f' +} + +# A wolfTrust global's address, from the image this run built and flashed. +elf_sym() { + arm-none-eabi-nm "$repo/build/wolftrust.elf" | + awk -v s="$1" '$3 == s && !f { print "0x" $1; f = 1 }' +} + +sym_word() { word_at "$(elf_sym "$1")" 0; } + +# Each bare-metal guest records its progress at the base of its own RAM +# window (tests/firmware/mcxn947-baremetal/guest0.c). +check_guest() { + local id="$1" base="$2" sig fw st uart + sig="$(word_at "$base" 0)" + fw="$(word_at "$base" 8)" + st="$(word_at "$base" 20)" + uart="$(word_at "$base" 24)" + check "$([ "$sig" = "47543030" ]; echo $?)" "guest$id launched: signature 0x47543030 ($sig)" + check "$([ "$fw" = "00000100" ]; echo $?)" "guest$id psa_framework_version 0x0100 ($fw)" + check "$([ "$st" = "600d600d" ]; echo $?)" \ + "guest$id done: FF-M connect verified, status 0x600D600D ($st)" + check "$([ -n "$uart" ] && [ "$uart" != "00000000" ]; echo $?)" \ + "guest$id reaches its Non-secure console (LPUART4 VERID 0x$uart)" +} + +# The peer keeps running: its idle beat advances between two reads. +check_beating() { + local id="$1" base="$2" b1 b2 + b1="$(word_at "$base" 36)" + sleep 1 + b2="$(word_at "$base" 36)" + check "$([ -n "$b1" ] && [ "$b1" != "$b2" ]; echo $?)" \ + "guest$id still running (beat 0x$b1 -> 0x$b2)" +} + +check_launch_masks() { + local want_verified="$1" want_refused="$2" verified refused + verified="$(sym_word g_wt_launch_verified_mask)" + refused="$(sym_word g_wt_launch_refused_mask)" + check "$([ "$verified" = "$want_verified" ]; echo $?)" \ + "launch-verified guest mask 0x$verified (want 0x$want_verified)" + check "$([ "$refused" = "$want_refused" ]; echo $?)" \ + "launch-refused guest mask 0x$refused (want 0x$want_refused)" +} + +# The Secure side of every run: the ROM flash API answered from the SVC gate, +# the vault was created in the ROM-programmed NVM, an erased phrase reads as +# 0xFF, wolfBoot's handoff was consumed, and the scheduler is not in a fault +# handler. +check_secure_side() { + local rom err phrase blank handoff ipsr lc + rom="$(sym_word g_wt_rom_flash_last_status)" + err="$(sym_word g_wt_flash_last_error)" + check "$([ "$rom" = "00000000" ]; echo $?)" \ + "ROM flash API reached from the SVC gate, last status 0x$rom" + check "$([ "$err" = "00000000" ]; echo $?)" "wolfHSM flash backend last error 0x$err" + phrase="$(word_at "$hsm_nvm_addr" 4)" + check "$([ -n "$phrase" ] && [ "$phrase" != "ffffffff" ]; echo $?)" \ + "the vault's first state phrase is programmed (0x$phrase)" + blank="$(word_at "$hsm_nvm_addr" "$flash_sector")" + check "$([ "$blank" = "ffffffff" ]; echo $?)" \ + "an erased phrase reads as 0xFF through the bus (0x$blank)" + handoff="$(word_at "$handoff_addr" 0)" + check "$([ "$handoff" = "00000000" ]; echo $?)" \ + "wolfBoot's handoff record was consumed and cleared (0x$handoff)" + ipsr="$(dap -c halt -c 'reg xpsr' -c go | sed -n 's/^xpsr = 0x\([0-9a-fA-F]*\).*/\1/p')" + ipsr=$((0x${ipsr:-3} & 0x1ff)) + check "$([ "$ipsr" -lt 2 ] || [ "$ipsr" -gt 7 ]; echo $?)" \ + "system still scheduling (IPSR $ipsr, not a fault handler)" + lc="$(word_at "$syscon_lc_state" 0)" + log " [lifecycle] ELS_OTP_LC_STATE 0x$lc" + check "$(grep -q "Verifying signature...done" "$work/console.log"; echo $?)" \ + "wolfBoot verified the wolfTrust image signature (console)" +} + +# guest0 faulted on every launch: the monitor spent its restart budget and +# quarantined it, while guest1 ran untouched. +check_guest0_quarantined() { + local restarts quarantines st beat + restarts="$(sym_word g_wt_restart_events)" + quarantines="$(sym_word g_wt_quarantine_events)" + check "$([ "$((0x$restarts))" -eq "$restart_limit" ]; echo $?)" \ + "guest0 relaunched through its restart budget ($((0x$restarts)) of $restart_limit)" + check "$([ "$((0x$quarantines))" -eq 1 ]; echo $?)" \ + "guest0 quarantined once ($((0x$quarantines)))" + st="$(word_at "$guest0_ram" 20)" + beat="$(word_at "$guest0_ram" 36)" + check "$([ "$st" != "600d600d" ]; echo $?)" "guest0 never reported done (status 0x$st)" + check "$([ "$beat" = "00000000" ]; echo $?)" "guest0 never reached its idle loop (beat 0x$beat)" + check_launch_masks 00000003 00000000 + check_guest 1 "$guest1_ram" + check_beating 1 "$guest1_ram" +} + +# The PSA guest's mailbox (tests/firmware/psa-guest/main.c): a signature and +# the lifecycle stages it completed. +check_psa_guest() { + local id="$1" base="$2" sig lc neg + sig="$(word_at "$base" 0)" + lc="$(word_at "$base" 4)" + neg="$(word_at "$base" 8)" + check "$([ "$sig" = "50534147" ]; echo $?)" "guest$id launched: signature PSAG ($sig)" + check "$([ $((0x$lc & 0xBF)) -eq $((0xBF)) ]; echo $?)" \ + "guest$id completed the TEE, crypto, ITS, PS, key-ops, SHA KAT and done stages (0x$lc)" + check "$([ $((0x$lc & 0x40)) -ne 0 ]; echo $?)" \ + "guest$id completed the attestation COSE stage (0x$lc)" + check "$([ "$neg" = "00000000" ]; echo $?)" "guest$id FF-M negatives all rejected (0x$neg)" +} + +case "$scenario" in +positive) + run_chain "$baremetal_dir" "" + check_guest 0 "$guest0_ram" + check_guest 1 "$guest1_ram" + check_launch_masks 00000003 00000000 + check "$([ "$(sym_word g_wt_restart_events)" = "00000000" ]; echo $?)" "no guest restarted" + check_secure_side + log "PASS: hardware/mcxn947/$scenario" + ;; +sauneg) + run_chain "$baremetal_dir" "WT_SAU_PROBE=1" + check_guest0_quarantined + peer="$(word_at "$probe_addr" 0)" + check "$([ "$peer" != "deadbeef" ]; echo $?)" \ + "guest1 RAM never received guest0's sentinel (0x$peer)" + check_secure_side + log "PASS: hardware/mcxn947/$scenario" + ;; +periphneg) + run_chain "$baremetal_dir" "WT_PERIPH_NEG_PROBE=1" + check_guest0_quarantined + check "$(! grep -q "periph probe LEAKED" "$work/console.log"; echo $?)" \ + "no NS read of the SPM's ELS ever returned (console)" + check_secure_side + log "PASS: hardware/mcxn947/$scenario" + ;; +restart) + run_chain "$baremetal_dir" "WT_GUEST_FAULT_PROBE=1" + check_guest0_quarantined + check_secure_side + log "PASS: hardware/mcxn947/$scenario" + ;; +psa) + run_chain "$psa_dir" "" + check_psa_guest 0 "$guest0_ram" + check_psa_guest 1 "$guest1_ram" + check_launch_masks 00000003 00000000 + check "$([ "$(sym_word g_wt_restart_events)" = "00000000" ]; echo $?)" "no guest restarted" + check_secure_side + log "PASS: hardware/mcxn947/$scenario" + ;; +*) + log "usage: $0 positive|sauneg|periphneg|restart|psa" + exit 2 + ;; +esac From 5a4e02c2db05af9926aa3670adb5f5481640b377 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Sat, 24 Oct 2026 06:09:58 +0100 Subject: [PATCH 13/45] Add make test-hardware for the FRDM-MCXN947 suite --- Makefile | 2 ++ tests/target/run_mcxn947_suite.sh | 40 +++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100755 tests/target/run_mcxn947_suite.sh diff --git a/Makefile b/Makefile index 28919f15..ec84c1dc 100644 --- a/Makefile +++ b/Makefile @@ -101,6 +101,8 @@ endif test-hardware: ifeq ($(TARGET),mimxrt700) @tests/target/run_rt700_suite.sh +else ifeq ($(TARGET),mcxn947) + @tests/target/run_mcxn947_suite.sh else @tests/target/run_h5_suite.sh endif diff --git a/tests/target/run_mcxn947_suite.sh b/tests/target/run_mcxn947_suite.sh new file mode 100755 index 00000000..011d430f --- /dev/null +++ b/tests/target/run_mcxn947_suite.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# wolfTrust MCXN947 hardware suite, the FRDM-MCXN947 sibling of +# run_rt700_suite.sh: the positive chain, the guest isolation and peripheral +# negatives, the restart budget, and the PSA client lifecycle, each built and +# flashed to the board on the host that owns its MCU-Link. Without a board +# detect_mcxn947.sh reports why and the suite SKIPs; it never silently passes. +set -euo pipefail + +repo="$(cd "$(dirname "$0")/../.." && pwd)" +runner="$repo/tests/target/run_mcxn947_hardware.sh" +scenarios="${WT_MCXN947_SCENARIOS:-positive sauneg periphneg restart psa}" +spsdk_venv="${MCXN947_SPSDK_VENV:-$HOME/spsdk-venv}" + +# The runner puts the SPSDK venv on PATH itself; detection needs it first. +if [ -d "$spsdk_venv/bin" ]; then + PATH="$spsdk_venv/bin:$PATH" + export PATH +fi +if ! "$repo/tests/target/detect_mcxn947.sh" >/dev/null 2>&1; then + echo "SKIP: MCXN947 hardware suite ($("$repo/tests/target/detect_mcxn947.sh" 2>&1))" + exit 0 +fi + +logs="$repo/build/mcxn947-suite" +mkdir -p "$logs" +rc=0 +for s in $scenarios; do + echo "RUN: hardware/$s" + if "$runner" "$s" >"$logs/$s.log" 2>&1; then + grep -F ' [check] ' "$logs/$s.log" || true + echo "PASS: hardware/$s" + else + grep -F ' [check] ' "$logs/$s.log" || true + echo "FAIL: hardware/$s (tail of $logs/$s.log):" + tail -15 "$logs/$s.log" || true + rc=1 + fi +done + +if [ "$rc" -eq 0 ]; then echo "PASS: hardware/all"; else echo "FAIL: hardware/all"; exit 1; fi From 96064fcefa5decb0489f0944c9c8832b69940258 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Sat, 24 Oct 2026 06:09:58 +0100 Subject: [PATCH 14/45] Model the MCXN947 ELS clock gate and executable ROM in M33MU --- tests/target/m33mu-mcxn947.patch | 99 ++++++++++++++++++++++++++++---- 1 file changed, 89 insertions(+), 10 deletions(-) diff --git a/tests/target/m33mu-mcxn947.patch b/tests/target/m33mu-mcxn947.patch index ae8d96a9..258b3b9c 100644 --- a/tests/target/m33mu-mcxn947.patch +++ b/tests/target/m33mu-mcxn947.patch @@ -1,20 +1,40 @@ diff --git a/cpu/mcxn947/mcxn947_mmio.c b/cpu/mcxn947/mcxn947_mmio.c -index 831de76..875d3cf 100644 +index 831de76..e279d77 100644 --- a/cpu/mcxn947/mcxn947_mmio.c +++ b/cpu/mcxn947/mcxn947_mmio.c -@@ -104,6 +104,8 @@ +@@ -4,6 +4,7 @@ + */ + #include + #include ++#include + #include "mcxn947/mcxn947_mmio.h" + #include "mcxn947/mcxn947_romapi.h" + #include "mcxn947/mcxn947_secure.h" +@@ -102,8 +103,12 @@ + + /* SYSCON register offsets */ #define SYSCON_PRESETCTRL0 0x100u ++#define SYSCON_PRESETCTRL2 0x108u #define SYSCON_AHBCLKCTRL0 0x200u ++#define SYSCON_AHBCLKCTRL2 0x208u #define SYSCON_AHBCLKCTRL1 0x204u +#define SYSCON_ELS_OTP_LC_STATE 0x980u +#define SYSCON_ELS_OTP_LC_STATE_DP 0x984u struct gpio_bank { mm_u32 regs[GPIO_SIZE / 4]; -@@ -676,6 +678,10 @@ void mm_mcxn947_mmio_reset(void) +@@ -674,8 +679,16 @@ void mm_mcxn947_mmio_reset(void) + g_gpio_nvic = 0; + /* Initialize SYSCON reset states */ - syscon.regs[SYSCON_AHBCLKCTRL0 / 4] = 0x021FFE603u; /* + ELS clock bit */ - syscon.regs[SYSCON_PRESETCTRL0 / 4] = 0x021FFE000u; /* + ELS reset released */ +- syscon.regs[SYSCON_AHBCLKCTRL0 / 4] = 0x021FFE603u; /* + ELS clock bit */ +- syscon.regs[SYSCON_PRESETCTRL0 / 4] = 0x021FFE000u; /* + ELS reset released */ ++ syscon.regs[SYSCON_AHBCLKCTRL0 / 4] = 0x021FFE603u; ++ syscon.regs[SYSCON_PRESETCTRL0 / 4] = 0x021FFE000u; ++ /* The ROM leaves the ELS (CSS, AHBCLKCTRL2/PRESETCTRL2 bit 18) clocked ++ * and out of reset. */ ++ syscon.regs[SYSCON_AHBCLKCTRL2 / 4] = (1u << 18); ++ syscon.regs[SYSCON_PRESETCTRL2 / 4] = (1u << 18); + /* ELS_OTP_LC_STATE and its duplicate: OEM_OPEN (0x03), the life cycle a + * FRDM board ships in, as the ROM reflects it from OTP. */ + syscon.regs[SYSCON_ELS_OTP_LC_STATE / 4] = 0x03u; @@ -22,6 +42,17 @@ index 831de76..875d3cf 100644 scg.regs[SCG_VERID / 4u] = 0x00000000u; scg.regs[SCG_PARAM / 4u] = 0x000001FEu; +@@ -774,6 +787,10 @@ static mm_bool syscon_write(void *opaque, mm_u32 offset, mm_u32 size_bytes, mm_u + if (s == 0 || size_bytes == 0 || size_bytes > 4) return MM_FALSE; + if ((offset + size_bytes) > SYSCON_SIZE) return MM_FALSE; + if (size_bytes != 4) return MM_FALSE; ++ if (offset < 0x250u && getenv("M33MU_MCXN947_SYSCON_TRACE")) { ++ printf("[SYSCON] write offset=0x%03lx value=0x%08lx\n", ++ (unsigned long)offset, (unsigned long)value); ++ } + s->regs[offset / 4] = value; + return MM_TRUE; + } diff --git a/cpu/mcxn947/mcxn947_romapi.c b/cpu/mcxn947/mcxn947_romapi.c index e98f570..3ac8b98 100644 --- a/cpu/mcxn947/mcxn947_romapi.c @@ -79,7 +110,7 @@ index e98f570..3ac8b98 100644 } romapi_persist_flush(flash_off, len); diff --git a/cpu/mcxn947/mcxn947_secure.c b/cpu/mcxn947/mcxn947_secure.c -index 148ef22..3f1941c 100644 +index 148ef22..d15bd66 100644 --- a/cpu/mcxn947/mcxn947_secure.c +++ b/cpu/mcxn947/mcxn947_secure.c @@ -26,6 +26,36 @@ @@ -141,7 +172,7 @@ index 148ef22..3f1941c 100644 mm_u8 *flash; mm_u32 flash_size; mm_u32 lifecycle; -@@ -686,6 +724,125 @@ static mm_bool secure_region_write(void *opaque, mm_u32 offset, mm_u32 size_byte +@@ -686,6 +724,141 @@ static mm_bool secure_region_write(void *opaque, mm_u32 offset, mm_u32 size_byte return MM_FALSE; } @@ -156,6 +187,10 @@ index 148ef22..3f1941c 100644 + if (g_sec.map == 0 || len == 0u || len > S50_RND_MAX || + (len % 4u) != 0u || (addr % 4u) != 0u) { + s->err_status |= S50_ERR_OPN; ++ if (getenv("M33MU_S50_TRACE")) { ++ printf("[S50] RND_REQ refused addr=0x%08lx len=%lu\n", ++ (unsigned long)addr, (unsigned long)len); ++ } + return; + } + mm_mcxn947_secure_rng_fill(block, len); @@ -163,9 +198,17 @@ index 148ef22..3f1941c 100644 + if (!mm_memmap_write(g_sec.map, MM_SECURE, addr + i, 4u, + read32le(block + i))) { + s->err_status |= S50_ERR_OPN; ++ if (getenv("M33MU_S50_TRACE")) { ++ printf("[S50] RND_REQ DMA write failed addr=0x%08lx\n", ++ (unsigned long)(addr + i)); ++ } + return; + } + } ++ if (getenv("M33MU_S50_TRACE")) { ++ printf("[S50] RND_REQ addr=0x%08lx len=%lu ok\n", ++ (unsigned long)addr, (unsigned long)len); ++ } +} + +static void s50_ctrl_write(struct mcxn947_s50_state *s, mm_u32 value) @@ -182,6 +225,10 @@ index 148ef22..3f1941c 100644 + } + if (!s->enabled || !mm_mcxn947_secure_els_clock_ready()) { + s->err_status |= S50_ERR_OPN; ++ if (getenv("M33MU_S50_TRACE")) { ++ printf("[S50] START refused enabled=%d clock_ready=%d\n", ++ (int)s->enabled, (int)mm_mcxn947_secure_els_clock_ready()); ++ } + return; + } + if (cmd == S50_CMD_RND_REQ) { @@ -267,7 +314,7 @@ index 148ef22..3f1941c 100644 static mm_bool secure_register_pair(struct mmio_bus *bus, mm_u32 base, struct mcxn947_secure_regs *regs) { -@@ -710,6 +867,7 @@ void mm_mcxn947_secure_reset(void) +@@ -710,6 +883,7 @@ void mm_mcxn947_secure_reset(void) secure_reset_regs(&g_sec.els); secure_reset_regs(&g_sec.pkc); secure_reset_regs(&g_sec.prince); @@ -275,7 +322,7 @@ index 148ef22..3f1941c 100644 g_sec.puf.status |= SEC_STATUS_DONE; g_sec.otpc.status |= SEC_STATUS_DONE; g_sec.els.status |= SEC_STATUS_DONE; -@@ -730,6 +888,7 @@ mm_bool mm_mcxn947_secure_register_mmio(struct mmio_bus *bus) +@@ -730,6 +904,7 @@ mm_bool mm_mcxn947_secure_register_mmio(struct mmio_bus *bus) if (!secure_register_pair(bus, MCXN947_ELS_BASE, &g_sec.els)) return MM_FALSE; if (!secure_register_pair(bus, MCXN947_PKC_BASE, &g_sec.pkc)) return MM_FALSE; if (!secure_register_pair(bus, MCXN947_PRINCE_BASE, &g_sec.prince)) return MM_FALSE; @@ -283,7 +330,7 @@ index 148ef22..3f1941c 100644 return MM_TRUE; } -@@ -739,9 +898,9 @@ void mm_mcxn947_secure_flash_bind(struct mm_memmap *map, +@@ -739,9 +914,9 @@ void mm_mcxn947_secure_flash_bind(struct mm_memmap *map, const struct mm_flash_persist *persist, mm_u32 flags) { @@ -294,3 +341,35 @@ index 148ef22..3f1941c 100644 g_sec.flash = flash; g_sec.flash_size = flash_size; secure_refresh_identity(&g_sec); +@@ -778,10 +953,11 @@ void mm_mcxn947_secure_rng_fill(mm_u8 *out, mm_u32 len) + } + } + ++/* The ELS (CSS) gate is AHBCLKCTRL2/PRESETCTRL2 bit 18 (SDK kCLOCK_Css). */ + mm_bool mm_mcxn947_secure_els_clock_ready(void) + { +- return mm_mcxn947_syscon_clock_bit_on(0x200u, 25u) && +- mm_mcxn947_syscon_reset_bit_released(0x100u, 25u); ++ return mm_mcxn947_syscon_clock_bit_on(0x208u, 18u) && ++ mm_mcxn947_syscon_reset_bit_released(0x108u, 18u); + } + + mm_bool mm_mcxn947_secure_rom_call_allowed(enum mm_sec_state sec) +diff --git a/src/main.c b/src/main.c +index aad0697..9457fa4 100644 +--- a/src/main.c ++++ b/src/main.c +@@ -6382,8 +6382,11 @@ int main(int argc, char **argv) + mm_prot_add_region(&prot, cfg.flash_base_ns, cfg.flash_size_ns, + MM_PROT_PERM_READ | MM_PROT_PERM_WRITE | MM_PROT_PERM_EXEC, MM_SECURE); + if (cpu_name != 0 && strcmp(cpu_name, "mcxn947") == 0) { +- mm_prot_add_region(&prot, 0x1303fc00u, 0x00000200u, MM_PROT_PERM_READ, MM_SECURE); +- mm_prot_add_region(&prot, 0x1303fc00u, 0x00000200u, MM_PROT_PERM_READ, MM_NONSECURE); ++ /* MCXN947 ROM at 0x03000000 (NS alias) / 0x13000000 (S alias): ++ * the ROM API stubs are fetched from it, so without an ++ * executable region every ROM call left IACCVIOL in CFSR. */ ++ mm_prot_add_region(&prot, 0x03000000u, 0x00040000u, MM_PROT_PERM_READ | MM_PROT_PERM_EXEC, MM_NONSECURE); ++ mm_prot_add_region(&prot, 0x13000000u, 0x00040000u, MM_PROT_PERM_READ | MM_PROT_PERM_EXEC, MM_SECURE); + } + if (cpu_name != 0 && strcmp(cpu_name, "lpc55s69") == 0) { + /* LPC55S69 ROM is at 0x03000000 (NS alias) / 0x13000000 (S alias). From a9ac8561fd437d2229fcef39eb803f1011065e57 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Sat, 24 Oct 2026 06:12:40 +0100 Subject: [PATCH 15/45] Count ELS refusals and assert them in the MCXN947 hardware runner --- port/mcxn947/rng_entropy.c | 7 +++++++ tests/target/run_mcxn947_hardware.sh | 5 ++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/port/mcxn947/rng_entropy.c b/port/mcxn947/rng_entropy.c index 75a0dd94..e684623a 100644 --- a/port/mcxn947/rng_entropy.c +++ b/port/mcxn947/rng_entropy.c @@ -37,6 +37,10 @@ int wolftrust_rng_generate_block_direct(unsigned char *output, #define WT_ELS_PAGE_WORDS 16u static bool s_rng_ready; +/* Read over the debug port by the hardware runner. */ +volatile uint32_t g_wt_els_errors __attribute__((used)); +volatile uint32_t g_wt_els_last_status __attribute__((used)); +volatile uint32_t g_wt_els_last_err __attribute__((used)); static int wt_els_wait_idle(void) { @@ -49,6 +53,9 @@ static int wt_els_wait_idle(void) } if ((status & (WT_ELS_STATUS_BUSY | WT_ELS_STATUS_ERR | WT_ELS_STATUS_LOCKED)) != 0u) { + g_wt_els_errors++; + g_wt_els_last_status = status; + g_wt_els_last_err = WT_ELS_ERR_STATUS; return -1; } return 0; diff --git a/tests/target/run_mcxn947_hardware.sh b/tests/target/run_mcxn947_hardware.sh index a73f8587..3159e9c4 100755 --- a/tests/target/run_mcxn947_hardware.sh +++ b/tests/target/run_mcxn947_hardware.sh @@ -314,12 +314,15 @@ check_launch_masks() { # 0xFF, wolfBoot's handoff was consumed, and the scheduler is not in a fault # handler. check_secure_side() { - local rom err phrase blank handoff ipsr lc + local rom err els phrase blank handoff ipsr lc rom="$(sym_word g_wt_rom_flash_last_status)" err="$(sym_word g_wt_flash_last_error)" check "$([ "$rom" = "00000000" ]; echo $?)" \ "ROM flash API reached from the SVC gate, last status 0x$rom" check "$([ "$err" = "00000000" ]; echo $?)" "wolfHSM flash backend last error 0x$err" + els="$(sym_word g_wt_els_errors)" + check "$([ "$els" = "00000000" ]; echo $?)" \ + "the ELS served every RND_REQ (refusals 0x$els, last status 0x$(sym_word g_wt_els_last_status), error 0x$(sym_word g_wt_els_last_err))" phrase="$(word_at "$hsm_nvm_addr" 4)" check "$([ -n "$phrase" ] && [ "$phrase" != "ffffffff" ]; echo $?)" \ "the vault's first state phrase is programmed (0x$phrase)" From 3ee9bd38f838f26b0b45e6ace132a0021df62294 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Sat, 24 Oct 2026 06:18:59 +0100 Subject: [PATCH 16/45] Build the MCXN947 life cycle hook outside wolfBoot's DICE-only block --- tests/target/wolfboot-mcxn947-lifecycle.patch | 42 ++++++++++++++----- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/tests/target/wolfboot-mcxn947-lifecycle.patch b/tests/target/wolfboot-mcxn947-lifecycle.patch index 6127ffb4..b77e45d8 100644 --- a/tests/target/wolfboot-mcxn947-lifecycle.patch +++ b/tests/target/wolfboot-mcxn947-lifecycle.patch @@ -1,4 +1,5 @@ -diff -ruN a/hal/mcxn.c b/hal/mcxn.c +diff --git a/hal/mcxn.c b/hal/mcxn.c +index 318eeed5..951955c3 100644 --- a/hal/mcxn.c +++ b/hal/mcxn.c @@ -25,6 +25,7 @@ @@ -9,32 +10,51 @@ diff -ruN a/hal/mcxn.c b/hal/mcxn.c #include "clock_config.h" #include "fsl_clock.h" -@@ -605,11 +606,22 @@ - return ret; +@@ -529,6 +530,25 @@ void uart_write(const char *buf, unsigned int sz) + } } +/* The PSA security lifecycle the Secure application attests, from the OTP + * life cycle state the ROM reflects into SYSCON and the debug authentication + * state. */ - int hal_attestation_get_lifecycle(uint32_t *lifecycle) - { ++int hal_attestation_get_lifecycle(uint32_t *lifecycle) ++{ + uint32_t lcState; + uint32_t lcStateDp; + uint32_t debugAuthStatus; + - if (lifecycle == NULL) - return -1; -- *lifecycle = 0x3000u; /* PSA_LIFECYCLE_SECURED (default) */ ++ if (lifecycle == NULL) ++ return -1; + lcState = *(volatile uint32_t *)MCXN_SYSCON_ELS_OTP_LC_STATE; + lcStateDp = *(volatile uint32_t *)MCXN_SYSCON_ELS_OTP_LC_STATE_DP; + debugAuthStatus = *(volatile uint32_t *)MCXN_DAUTHSTATUS_ADDRESS; + *lifecycle = mcxn_attestation_lifecycle(lcState, lcStateDp, + debugAuthStatus); - return 0; ++ return 0; ++} ++ + #if defined(WOLFCRYPT_TZ_PSA) && defined(WOLFBOOT_DICE_HW) && defined(__WOLFBOOT) + + /* Holds the raw 64-byte P-256 public key (X||Y) written by hal_dice_create_attest_key(). +@@ -605,14 +625,6 @@ int hal_attestation_get_ueid(uint8_t *buf, size_t *len) + return ret; } -diff -ruN a/hal/mcxn_lifecycle.h b/hal/mcxn_lifecycle.h ---- a/hal/mcxn_lifecycle.h +-int hal_attestation_get_lifecycle(uint32_t *lifecycle) +-{ +- if (lifecycle == NULL) +- return -1; +- *lifecycle = 0x3000u; /* PSA_LIFECYCLE_SECURED (default) */ +- return 0; +-} +- + /* Counts actual hardware CDI derivations performed (wolfBoot is always skipped). + * Shared with hal_dice_create_attest_key so it can reset after each complete sequence. */ + static int cdi_derivation_count = 0; +diff --git a/hal/mcxn_lifecycle.h b/hal/mcxn_lifecycle.h +new file mode 100644 +index 00000000..aada6089 +--- /dev/null +++ b/hal/mcxn_lifecycle.h @@ -0,0 +1,136 @@ +/* mcxn_lifecycle.h From 197fe1f06d0714c4c2000d85dcbb4b165f5a56f9 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Sat, 24 Oct 2026 06:26:15 +0100 Subject: [PATCH 17/45] Check the PSA negatives mask and the handed life cycle in the MCXN947 runner --- tests/target/run_mcxn947_hardware.sh | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/target/run_mcxn947_hardware.sh b/tests/target/run_mcxn947_hardware.sh index 3159e9c4..5bb9ae26 100755 --- a/tests/target/run_mcxn947_hardware.sh +++ b/tests/target/run_mcxn947_hardware.sh @@ -314,7 +314,7 @@ check_launch_masks() { # 0xFF, wolfBoot's handoff was consumed, and the scheduler is not in a fault # handler. check_secure_side() { - local rom err els phrase blank handoff ipsr lc + local rom err els phrase blank handoff ipsr lc handed rom="$(sym_word g_wt_rom_flash_last_status)" err="$(sym_word g_wt_flash_last_error)" check "$([ "$rom" = "00000000" ]; echo $?)" \ @@ -338,6 +338,13 @@ check_secure_side() { "system still scheduling (IPSR $ipsr, not a fault handler)" lc="$(word_at "$syscon_lc_state" 0)" log " [lifecycle] ELS_OTP_LC_STATE 0x$lc" + handed="$(word_at "$(elf_sym g_wt_boot_handoff)" 12)" + if [ "$lc" = "00000003" ]; then + check "$([ "$handed" = "00001000" ]; echo $?)" \ + "wolfBoot handed OEM_OPEN as PSA ASSEMBLY_AND_TEST (lifecycle 0x$handed)" + else + log " [lifecycle] handoff lifecycle 0x$handed" + fi check "$(grep -q "Verifying signature...done" "$work/console.log"; echo $?)" \ "wolfBoot verified the wolfTrust image signature (console)" } @@ -373,7 +380,8 @@ check_psa_guest() { "guest$id completed the TEE, crypto, ITS, PS, key-ops, SHA KAT and done stages (0x$lc)" check "$([ $((0x$lc & 0x40)) -ne 0 ]; echo $?)" \ "guest$id completed the attestation COSE stage (0x$lc)" - check "$([ "$neg" = "00000000" ]; echo $?)" "guest$id FF-M negatives all rejected (0x$neg)" + check "$([ "$neg" = "0000000f" ]; echo $?)" \ + "guest$id FF-M negatives all rejected: forged handle, oversized vector, cross-guest vector, unknown SID (0x$neg)" } case "$scenario" in From e8aae0587aec29034c0985ec913db35802002991 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Sat, 24 Oct 2026 06:32:23 +0100 Subject: [PATCH 18/45] Document the MCXN947 silicon bring-up and hardware runner --- docs/MCXN947-Guide.md | 127 +++++++++++++++++++++++++++++++++--------- docs/Testing.md | 28 ++++++++-- 2 files changed, 123 insertions(+), 32 deletions(-) diff --git a/docs/MCXN947-Guide.md b/docs/MCXN947-Guide.md index d2f3b57c..893eb31a 100644 --- a/docs/MCXN947-Guide.md +++ b/docs/MCXN947-Guide.md @@ -12,7 +12,19 @@ the part; this port rehearses life cycles and writes none of them. - **Validated on silicon:** the wolfBoot first stage for this part (wolfSSL/wolfBoot#948: signed boot with ECC256/SHA-256, update swap and rollback over the write-once trailer, and the measured Secure-application - handoff to `0x30030000`). + handoff to `0x30030000`), and the wolfTrust chain behind it on the + FRDM-MCXN947 (`tests/target/run_mcxn947_hardware.sh`, see + [Build and verify on the board](#build-and-verify-on-the-frdm-mcxn947)): + wolfBoot verifies the Secure image, the SPM consumes the handoff, creates + the wolfHSM vault in the ROM-programmed NVM, and launches both guests on + both crypto engines (`positive`); the SAU refuses a guest's store into its + peer's RAM (`sauneg`) and its read of the SPM's ELS (`periphneg`), and a + guest faulting at launch spends its restart budget and is quarantined + (`restart`), the peer running on throughout; the portable PSA client runs + its crypto, ITS, sealed PS, key-ops, FF-M negative and SHA-256 KAT stages + from both windows with the ELS behind `psa_generate_random`, and verifies + the attestation token it is issued against the IAK the vault provisioned at + first boot (`psa`). - **Validated in cross-build:** the wolfTrust Secure image (`make TARGET=mcxn947 secure-image`) on both crypto engines and both manifests, with the port split, veneer, owner map, and layout checks green. @@ -26,10 +38,9 @@ the part; this port rehearses life cycles and writes none of them. scenarios the matrix lists for this port (`tests/target/lib/scenario_matrix.py`) pass on the model. Scenarios the matrix exempts have not been run. -- **Not validated:** nothing of the wolfTrust Secure image has run on the - FRDM board yet; there is no hardware runner, no guest flash fence, and no - life cycle write. The TRDC and the AHB secure controller are left as the - first loader leaves them. See [Open items](#open-items). +- **Not validated:** any guest flash fence and any life cycle write. The + TRDC and the AHB secure controller are left as the first loader leaves + them. See [Open items](#open-items). Record emulator, cross-build, and physical-board evidence separately: M33MU's MCXN947 model gives emulator evidence, the FRDM board gives silicon evidence, @@ -64,7 +75,7 @@ Armv8-M isolation level 3 layer in `port/common/armv8m/` unchanged, and adds | `mcxn947_regs.h` | Register bases for SYSCON, the FMU, the ELS, LPUART4, the TRDC and the AHBSC, the boot ROM and its API tree, and the ELS command interface. | | `platform_mcxn947.c` | Every `wt_platform_*` operation: the SAU table, the Secure MPU whitelist (eight regions, one of them the boot ROM for the flash API), the boot-handoff region, fault logging, panic, and reset. | | `partitions.c` | The guest and capability tables, the profile capability bitmap (the fabric filter is claimed on the per-dispatch SAU window, as on the MIMXRT700), and the pinned guest-measurement slot. | -| `rom_flash.c/.h` | Internal flash program and erase through the boot ROM's API tree, called directly with a local definition of the driver interface layout (no vendor SDK): one 16-byte phrase is programmed at most once per erase, every erase is verified by the ROM, and the code-bus cache is cleared after each change. | +| `rom_flash.c/.h` | Internal flash program and erase through the boot ROM's API tree, called directly with a local definition of the driver interface layout (no vendor SDK): one 16-byte phrase is programmed at most once per erase, each program is issued from its 128-byte page base behind all-ones padding as the ROM requires, every erase is verified by the ROM, and the code-bus cache is cleared after each change. | | `hsm_flash.c/.h`, `hsm_flash_ctx.c` | The `port_nvm.h` backend for the wolfHSM store and the FWU staging backend: reads through the Secure alias, program and erase through `rom_flash.c`. | | `rng_entropy.c` | The `CUSTOM_RAND_GENERATE_BLOCK` entropy source over the ELS `RND_REQ` command, preserving the unprivileged-to-privileged trap. | | `secure.ld` | The port's Secure linker script, which INCLUDEs the shared level 3 regions, band sections, and layout ASSERTs; the SG veneers sit in the image's second 1 KiB, as on the STM32H5. | @@ -148,20 +159,34 @@ is scheduling policy, not a boundary. The SAU window is the boundary, and `rom_flash.c` refuses to program a non-blank phrase and `hsm_flash.c` refuses any program that is not phrase-aligned. `tests/host/mcxn947_flash` runs the real driver and the wolfHSM store over a model of this flash and - proves it: 554 phrase programs across three partition compactions and a - reopen, zero double programs with 16-byte units (even through a backend - with no phrase guard), 281 double programs with wolfHSM's 8-byte default, - which the port backend refuses outright. + its ROM and proves it: 643 page-based ROM programs across three partition + compactions and a reopen, zero double programs with 16-byte units (even + through a backend with no phrase guard), 281 double programs with + wolfHSM's 8-byte default, which the port backend refuses outright. +- **The ROM programs from a page base.** On silicon (ROM `K` 3.2.0) the + flash driver's `FLASH_Program` returns `kStatus_FLASH_AlignmentError` (101) + for any start that is not 128-byte page aligned, whatever the length, and + leaves an all-ones phrase exactly as it was (programming only clears bits, + and the erased phrase is a valid ECC word). `rom_flash.c` therefore + programs each phrase from its page base behind all-ones padding, one page + per ROM call; the padding never counts as a program of the phrases it + covers, which the host model checks. The ROM also stores each phrase + through the flash's Non-secure alias for the FMU to capture, so the SPM's + MPU keeps that alias writable (a store outside a ROM program sequence does + not alter flash). - **Flash is programmed through the ROM.** There is no SDK in the build; the port calls the ROM API tree at `0x1303FC00` through a local definition of its layout, checks the geometry the ROM reports (8 KiB sectors, 2 MiB) before the first program, masks interrupts around each call, and clears the LPCAC afterwards. Reads, verifies, and blank checks go through the - Secure alias, as wolfBoot's own flash path does on silicon. + Secure alias, as wolfBoot's own flash path does on silicon; an erased + phrase reads as `0xFF` without a fault. - **Entropy is the ELS.** `RND_REQ` (command 24) fills a buffer by DMA; the port requests whole pages, serves only what a request uses, wipes the rest, - and fails closed on `ELS_ERR` or a locked ELS. The ROM leaves the ELS - clocked; the port enables it and never resets it. + and fails closed on `ELS_ERR` or a locked ELS, counting every refusal in + `g_wt_els_errors` for the hardware runner. The ROM leaves the ELS clocked + (`AHBCLKCTRL2`/`PRESETCTRL2` bit 18); the port enables it and never resets + it. - **The first loader's image header sets the Secure link address.** wolfBoot on this part uses a 1024-byte image header, so wolfTrust links at the boot base plus `0x400` (`WT_SECURE_IMAGE_HEADER_SIZE=0x400`) and is signed with @@ -176,9 +201,10 @@ is scheduling policy, not a boundary. The SAU window is the boundary, and - Python 3, CMake (for the emulator build) - wolfBoot's MCXN HAL builds against the MCUXpresso SDK (`MCUXPRESSO`, `MCUXPRESSO_CMSIS`, or `~/NXP` as wolfBoot's config expects) -- for a board: the FRDM-MCXN947 with its MCU-Link, pyOCD with the MCXN947 - pack, and SPSDK's `blhost` (`tests/target/detect_mcxn947.sh` reports what - is missing); no hardware runner exists yet +- for a board: the FRDM-MCXN947 with its MCU-Link (CMSIS-DAP and VCOM), + pyOCD with the MCXN947 pack (`pyocd pack install MCXN947`), and SPSDK's + `blhost` for the life cycle rehearsal (`tests/target/detect_mcxn947.sh` + reports what is missing) ## Build and verify under M33MU @@ -191,7 +217,10 @@ tests/target/run_mcxn947_m33mu.sh sauneg The runner builds its pinned emulator (upstream M33MU plus `tests/target/m33mu-mcxn947.patch`, which adds the ELS `RND_REQ` register -model and reflects the OEM_OPEN life cycle into SYSCON) and its pinned +model behind its `AHBCLKCTRL2` gate, programs flash from a page base by +clearing bits as the ROM does, makes the ROM executable so an API call leaves +no stale `IACCVIOL` in `CFSR`, and reflects the OEM_OPEN life cycle into +SYSCON) and its pinned wolfBoot (`tests/target/lib/mcxn947_wolfboot.sh`: the wolfSSL/wolfBoot#948 branch at its fork ref until it merges, built from `config/examples/mcxn-tz-wolftrust.config` with the carried life cycle @@ -235,6 +264,44 @@ own Non-secure MPU and stores a sentinel into guest 1's RAM; the runner asserts from M33MU's protection-unit trace that the store faulted, refused by the SAU with the peer window Secure at dispatch. +## Build and verify on the FRDM-MCXN947 + +```sh +make test-hardware TARGET=mcxn947 # positive sauneg periphneg restart psa +tests/target/run_mcxn947_hardware.sh positive +WT_ENGINE=hsm tests/target/run_mcxn947_hardware.sh positive +``` + +`tests/target/run_mcxn947_hardware.sh` runs on the host that owns the +board's MCU-Link (`MCXN947_PROBE_UID` picks one of several probes; the +console is the MCU-Link VCOM, `MCXN947_CONSOLE`). For each scenario it +builds the Secure image and the guests, pins the guest measurements, signs +the image with the pinned wolfBoot's key, flashes wolfBoot, the signed image +and both guests at their physical offsets with pyOCD, erases the update, +swap and NVM sectors so every run starts from a blank vault, clears the +mailboxes and launch masks in SRAM, boots, reads every image back and +compares it, and asserts the result over SWD: + +- the bare-metal mailboxes (signature, framework version, `0x600D600D`, + the LPUART4 `VERID` the guest read) and, for the negatives, the monitor's + `g_wt_restart_events` and `g_wt_quarantine_events` counters, the launch + masks, the peer's advancing idle beat, and that the sentinel never landed; +- the Secure side of every run: `g_wt_rom_flash_last_status` 0 (the ROM + flash API answered from the SVC gate), the wolfHSM backend's last error, + the vault's first state phrase programmed, an erased phrase reading as + `0xFF`, the handoff record consumed, IPSR outside the fault handlers, and + wolfBoot's `Verifying signature...done` on the console; +- for `psa`, each PSA guest's mailbox stage bits (TEE, crypto, ITS, PS, + key-ops, SHA-256 KAT, COSE attestation, done). + +Two board facts the runner encodes: the pack's `reset halt` leaves a DWT +comparator and the debug fault status armed, so the core is released only +after both are cleared; and the MCU-Link forwards early-boot UART only while +the debug link stays active, so the console capture runs alongside spaced +reads. The two guests share LPUART4, so their console lines interleave on +silicon; the assertions are the mailboxes and counters, the console is +evidence. + ## Provisioning and life cycle This section is the MCXN947 part of [Provisioning](Provisioning.md). The @@ -284,13 +351,12 @@ other ports' gates. ## Open items -- **Silicon bring-up.** The Secure image, the ELS entropy path, the ROM - flash API calls from the privileged SVC gate, the LPCAC clear, and the - Secure MPU map (including executing the ROM through its Secure alias) are - emulator-validated only. The first board run should check the attribution - of every Secure peripheral the SPM uses and that reading an erased phrase - through the alias returns `0xFF` without a fault, which wolfBoot's flash - path on this part already relies on. +- **Life cycle in the handoff.** wolfBoot's HAL at the pin reports no life + cycle (`hal_attestation_get_lifecycle` is the weak default), so the + carried patch supplies it; the hardware runner checks the handed value + against `ELS_OTP_LC_STATE` on every run because an `UNKNOWN` life cycle + silently fails attestation closed (the vault may not provision an IAK + under it). - **Guest flash fence.** No hardware write protection of the guest windows is claimed: `wt_platform_guest_flash_wrp_ok` reports every window unprotected, so a `WT_GUEST_FLASH_WRP=1` image refuses every guest. The TRDC MBC gates @@ -301,11 +367,18 @@ other ports' gates. - **Other bus masters.** CPU1, DMA, and the ELS DMA are not fenced from Secure RAM; programming the TRDC and the AHB secure controller for them is silicon work that M33MU does not model. -- **Hardware runner and lock.** `run_mcxn947_hardware.sh`, the hardware - suite, and a validated life cycle write do not exist yet. +- **wolfBoot's own flash writes.** The MCXN HAL at the pin programs 4-byte + words at any word-aligned address, which this ROM refuses off a page base + (`kStatus_FLASH_AlignmentError`, checked on the board with a direct ROM + call); wolfTrust's own stores never go through it, the hardware runner + never asks wolfBoot to write, and an update swap on silicon is not claimed + until the HAL programs from page bases. +- **Life cycle write.** A validated life cycle advance on the part does not + exist; the hardware runner rehearses only. - **Upstream pins.** The wolfBoot pin moves to wolfSSL/wolfBoot once wolfSSL/wolfBoot#948 merges; the emulator patch goes once M33MU models the - ELS command interface and seeds the life cycle itself. + ELS command interface, the ROM's page-based bit-clearing program, an + executable ROM, and seeds the life cycle itself. See [Porting](Porting.md) for the generic port contract, [Testing](Testing.md) for scenario selection, and [Security Model](Security-Model.md) for the policy diff --git a/docs/Testing.md b/docs/Testing.md index b6e6526c..621d02a4 100644 --- a/docs/Testing.md +++ b/docs/Testing.md @@ -396,9 +396,10 @@ recorded separately. M33MU also models the MCXN947 (`--cpu mcxn947`: internal flash, the boot ROM flash API tree, and, with `tests/target/m33mu-mcxn947.patch`, the ELS -`RND_REQ` register model and the OEM_OPEN life cycle in SYSCON), so the -MCXN947 port has the same emulator gate as the MIMXRT700, with the same -scenario names: +`RND_REQ` register model behind its real clock gate, the ROM's page-based +bit-clearing flash program, an executable ROM, and the OEM_OPEN life cycle +in SYSCON), so the MCXN947 port has the same emulator gate as the MIMXRT700, +with the same scenario names: ```sh make test-target TARGET=mcxn947 @@ -422,8 +423,25 @@ wolfBoot build does. `MCXN947_WOLFBOOT_DIR` reuses a tree the library built. The flash image offsets are physical: wolfBoot at 0, the signed Secure image at `0x40000`, the guests at `0x80000` and `0x100000`. -These are emulator results; nothing of the wolfTrust chain has run on the -FRDM-MCXN947 yet, and the port has no hardware runner. +These are emulator results; the board's are in the next section, recorded +separately. + +## MCXN947 hardware + +`make test-hardware TARGET=mcxn947` runs `tests/target/run_mcxn947_suite.sh` +on the host that owns the FRDM-MCXN947's MCU-Link: `positive`, `sauneg`, +`periphneg`, `restart`, and `psa` by default (`WT_MCXN947_SCENARIOS` +overrides the list; `WT_ENGINE` selects the crypto engine), each built, +flashed at its physical offset, erased to a fresh vault, read back, and +asserted over the debug port (`tests/target/run_mcxn947_hardware.sh`; the +MCXN947 Guide lists what each scenario asserts). The two guests share one +LPUART, so their console lines interleave on silicon; the console is captured +as evidence and the assertions are the mailboxes and the monitor's counters. +Without the probe, console, pyOCD pack, or SPSDK, `tests/target/detect_mcxn947.sh` +reports why and the suite skips. The device life cycle flow is driven +separately through `TARGET=mcxn947 tests/target/provisioning/provisioning_ctrl.sh` +and writes no fuse; see the MCXN947 Guide. Hardware test commands write flash +and reset the board. ## MIMXRT700 hardware From 71df3a1c0555e3545614d1528a3d728855c6589a Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Sat, 24 Oct 2026 06:33:10 +0100 Subject: [PATCH 19/45] List the MCXN947 hardware suite among the validation entry points --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 0f45bbb6..a7109177 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,7 @@ make test-target TARGET=mimxrt700 make test-target TARGET=mcxn947 make test-conformance WT_H5_DOCKER_IMAGE=ghcr.io/wolfssl/wolfboot-ci-m33mu:v1.15 make test-hardware +make test-hardware TARGET=mcxn947 ``` `make test-target` runs the port's smoke tier under M33MU (`WT_TIER=full` for From 710e445bdf36037694255f6bfdf9e343c116fdf0 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Mon, 26 Oct 2026 15:19:24 +0000 Subject: [PATCH 20/45] Claim every MCXN947 scenario that passes on M33MU --- docs/MCXN947-Guide.md | 12 ++++++--- tests/target/lib/scenario_matrix.py | 40 +++++++++++++++++++++++------ 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/docs/MCXN947-Guide.md b/docs/MCXN947-Guide.md index 893eb31a..5b1bc90a 100644 --- a/docs/MCXN947-Guide.md +++ b/docs/MCXN947-Guide.md @@ -34,10 +34,14 @@ the part; this port rehearses life cycles and writes none of them. through the veneers (`positive`, both engines), the isolation negative (`sauneg`) shows a guest's store into the other guest's RAM refused by the SAU, contained by the monitor, and the offender quarantined after its - restart budget while the peer keeps running, and the isolation level 3 - scenarios the matrix lists for this port - (`tests/target/lib/scenario_matrix.py`) pass on the model. Scenarios the - matrix exempts have not been run. + restart budget while the peer keeps running, and every other scenario the + matrix lists for this port (`tests/target/lib/scenario_matrix.py`: the + whole MIMXRT700 set, every isolation level 3 scenario included) passes on + the model on both crypto engines. The one MIMXRT700 scenario not listed is + `confboot`: the Arm FF-M IPC suite did not reach its exit breakpoint within + the 900 s budget on this model, while the dev_apis storage, crypto and + attestation suites and the vault recovery runs do; it is being run again + with a longer budget before it is claimed. - **Not validated:** any guest flash fence and any life cycle write. The TRDC and the AHB secure controller are left as the first loader leaves them. See [Open items](#open-items). diff --git a/tests/target/lib/scenario_matrix.py b/tests/target/lib/scenario_matrix.py index afe2fae2..744f7b1a 100755 --- a/tests/target/lib/scenario_matrix.py +++ b/tests/target/lib/scenario_matrix.py @@ -162,18 +162,42 @@ "label": "ci:mcxn947", "arm": "mcxn947-m33mu", "image": "ghcr.io/wolfssl/wolfboot-ci-m33mu:v1.25", - "smoke": ("positive", "sauneg", "crossdomain"), + "smoke": ("positive", "sauneg", "crossdomain", "bothpsa", "devcrypto"), "groups": ( ("positive", "MCXN947 positive lifecycle (SAU guest windows)"), ("sauneg", "MCXN947 cross-guest store faults and is contained"), - ("crossdomain", "MCXN947 SP domain isolation negative (L3)"), + ("restart authneg", "MCXN947 guest restart budget and launch refusal"), + ("rollbackneg remeasureneg manifestneg spbudgetneg", + "MCXN947 secure verdict negatives"), + ("crossdomain keystoreneg", "MCXN947 SP domain isolation negatives"), + ("spfaultneg panicneg", "MCXN947 SP fault and panic recovery"), + ("fpneg", "MCXN947 FP isolation (partition FP faults, contained)"), + ("sealbootneg", "MCXN947 damaged main-stack seal refuses to boot"), + ("bothpsa bothiso", "MCXN947 both-guest PSA lifecycle and isolation"), + ("attestneg fwustage", + "MCXN947 attestation negatives and FWU staging"), + ("hsmattackneg", + "MCXN947 wolfHSM cross-namespace + NVM relay negatives"), + ("devstorage devattest devattestqcbor", + "MCXN947 dev_apis storage and attestation conformance"), + ("devcrypto vaultrecover vaultrecoversec", + "MCXN947 dev_apis crypto conformance and vault recovery"), + ("periphneg", "MCXN947 NS read of the SPM's ELS refused by the SAU"), + ("deputyneg", "MCXN947 keystore-flash privileged deputy refused (L3)"), + ("hsmpinneg hsmfaultneg", + "MCXN947 wolfHSM pointer pin and tasklet fault containment (L3)"), + ("bandneg1 bandneg2 bandneg3 bandneg4 bandneg5 bandneg6", + "MCXN947 partition band-to-band isolation negatives (L3)"), + ("restartneg1 restartneg2 restartneg3", + "MCXN947 partitions restart on a reset band (L3)"), + ("periphspneg manifestneg2 manifestneg3", + "MCXN947 SPM peripheral and manifest level negatives (L3)"), + ("sealneg sealhaltneg sealpivotneg", + "MCXN947 stack seal containment and halt (L3)"), + ("mspovfneg xnneg svcneg", + "MCXN947 SPM stack, execute-never and SVC misuse (L3)"), ), - # Port bring-up: only scenarios run on the MCXN947 M33MU model are - # listed; each level 3 scenario leaves this map the day it is run. - "l3_exempt": { - s: "not yet run on the MCXN947 M33MU model" - for s in sorted(L3_REQUIRED) if s != "crossdomain" - }, + "l3_exempt": {}, }, } From 4ae58484d749c4a28adc350d609e02be10c3d3db Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Mon, 26 Oct 2026 15:22:36 +0000 Subject: [PATCH 21/45] Keep the MCXN947 hardware evidence outside build and serialize batches --- Makefile | 1 + docs/MCXN947-Guide.md | 5 ++ docs/Testing.md | 6 ++ tests/target/run_mcxn947_hardware.sh | 21 +++++-- tests/target/run_mcxn947_suite.sh | 24 ++++++-- tests/target/test_mcxn947_suite_evidence.sh | 68 +++++++++++++++++++++ 6 files changed, 115 insertions(+), 10 deletions(-) create mode 100755 tests/target/test_mcxn947_suite_evidence.sh diff --git a/Makefile b/Makefile index ec84c1dc..bd00d837 100644 --- a/Makefile +++ b/Makefile @@ -43,6 +43,7 @@ test: @python3 tests/scripts/test_m33mu_console.py @$(MAKE) --no-print-directory -C tests/host test @$(MAKE) --no-print-directory test-provisioning + @tests/target/test_mcxn947_suite_evidence.sh # Production lock gates of both provisioning backends, against stub tools. test-provisioning: diff --git a/docs/MCXN947-Guide.md b/docs/MCXN947-Guide.md index 5b1bc90a..d9c88781 100644 --- a/docs/MCXN947-Guide.md +++ b/docs/MCXN947-Guide.md @@ -279,6 +279,7 @@ WT_ENGINE=hsm tests/target/run_mcxn947_hardware.sh positive `tests/target/run_mcxn947_hardware.sh` runs on the host that owns the board's MCU-Link (`MCXN947_PROBE_UID` picks one of several probes; the console is the MCU-Link VCOM, `MCXN947_CONSOLE`). For each scenario it +takes the board lock (`logs/mcxn947-hw.lock`, shared with the suite), then builds the Secure image and the guests, pins the guest measurements, signs the image with the pinned wolfBoot's key, flashes wolfBoot, the signed image and both guests at their physical offsets with pyOCD, erases the update, @@ -298,6 +299,10 @@ compares it, and asserts the result over SWD: - for `psa`, each PSA guest's mailbox stage bits (TEE, crypto, ITS, PS, key-ops, SHA-256 KAT, COSE attestation, done). +The console capture, the readback and the wolfBoot build log are kept under +`logs/mcxn947-hw/`, the suite's per-scenario logs under `logs/mcxn947-suite/`: +outside `build/`, which every run cleans before it builds. + Two board facts the runner encodes: the pack's `reset halt` leaves a DWT comparator and the debug fault status armed, so the core is released only after both are cleared; and the MCU-Link forwards early-boot UART only while diff --git a/docs/Testing.md b/docs/Testing.md index 621d02a4..35c3e864 100644 --- a/docs/Testing.md +++ b/docs/Testing.md @@ -437,6 +437,12 @@ asserted over the debug port (`tests/target/run_mcxn947_hardware.sh`; the MCXN947 Guide lists what each scenario asserts). The two guests share one LPUART, so their console lines interleave on silicon; the console is captured as evidence and the assertions are the mailboxes and the monitor's counters. +The suite's logs (`logs/mcxn947-suite/`) and the runner's retained evidence +(`logs/mcxn947-hw/`: per-scenario console, readback, wolfBoot build log) +live outside `build/`, which every run cleans before it builds, and one batch +at a time owns the board through `logs/mcxn947-hw.lock` (`flock`, shared by +the runner); `tests/target/test_mcxn947_suite_evidence.sh` checks both under +`make test`. Without the probe, console, pyOCD pack, or SPSDK, `tests/target/detect_mcxn947.sh` reports why and the suite skips. The device life cycle flow is driven separately through `TARGET=mcxn947 tests/target/provisioning/provisioning_ctrl.sh` diff --git a/tests/target/run_mcxn947_hardware.sh b/tests/target/run_mcxn947_hardware.sh index 5bb9ae26..cc6a9bcc 100755 --- a/tests/target/run_mcxn947_hardware.sh +++ b/tests/target/run_mcxn947_hardware.sh @@ -36,7 +36,10 @@ set -euo pipefail scenario="${1:-}" here="$(cd "$(dirname "$0")" && pwd)" repo="$(cd "$here/../.." && pwd)" -work="${MCXN947_WORK:-$repo/build/mcxn947-hw}" +# Retained evidence (console, readback, wolfBoot build log) lives outside +# build/, which every run cleans before it builds. +work="${MCXN947_WORK:-$repo/logs/mcxn947-hw}" +lock="${MCXN947_HW_LOCK:-$repo/logs/mcxn947-hw.lock}" target="${MCXN947_TARGET:-mcxn947vdf}" probe_uid="${MCXN947_PROBE_UID:-}" console="${MCXN947_CONSOLE:-}" @@ -66,6 +69,14 @@ psa_dir="$repo/tests/firmware/psa-guest" # shellcheck source=lib/mcxn947_wolfboot.sh disable=SC1091 . "$here/lib/mcxn947_wolfboot.sh" +# One run at a time owns the board; a suite batch takes the same lock once +# for all of its scenarios and says so. +if [ -z "${MCXN947_HW_LOCKED:-}" ]; then + mkdir -p "$(dirname "$lock")" + exec 9>"$lock" + flock 9 +fi + log() { printf '%s\n' "$*"; } stage() { printf ' ... %s\n' "$*"; } fail() { log "FAIL: $*"; exit 1; } @@ -162,7 +173,7 @@ clear_mailboxes() { release_core() { local n stty -F "$console" 115200 raw -echo -crtscts -ixon 2>/dev/null || true - ( timeout "$console_seconds" cat "$console" > "$work/console.log" 2>/dev/null ) & + ( timeout "$console_seconds" cat "$console" > "$work/console-$scenario.log" 2>/dev/null ) & sleep 0.5 timeout 60 pyocd cmd -u "$probe_uid" -t "$target" -O resume_on_disconnect=false \ -O reset_type=hw -c "reset halt" -c "write32 0xE0001028 0" \ @@ -256,7 +267,7 @@ run_chain() { verify_at "$secure_flash_addr" "$repo/build/wolftrust_v1_signed.bin" verify_at "$guest0_flash_addr" "$guest_build/guest0.bin" verify_at "$guest1_flash_addr" "$guest_build/guest1.bin" - log " [console] $work/console.log" + log " [console] $work/console-$scenario.log" } # One 32-bit word at base+offset over SWD, as eight lowercase hex digits. @@ -345,7 +356,7 @@ check_secure_side() { else log " [lifecycle] handoff lifecycle 0x$handed" fi - check "$(grep -q "Verifying signature...done" "$work/console.log"; echo $?)" \ + check "$(grep -q "Verifying signature...done" "$work/console-$scenario.log"; echo $?)" \ "wolfBoot verified the wolfTrust image signature (console)" } @@ -406,7 +417,7 @@ sauneg) periphneg) run_chain "$baremetal_dir" "WT_PERIPH_NEG_PROBE=1" check_guest0_quarantined - check "$(! grep -q "periph probe LEAKED" "$work/console.log"; echo $?)" \ + check "$(! grep -q "periph probe LEAKED" "$work/console-$scenario.log"; echo $?)" \ "no NS read of the SPM's ELS ever returned (console)" check_secure_side log "PASS: hardware/mcxn947/$scenario" diff --git a/tests/target/run_mcxn947_suite.sh b/tests/target/run_mcxn947_suite.sh index 011d430f..1a48a99f 100755 --- a/tests/target/run_mcxn947_suite.sh +++ b/tests/target/run_mcxn947_suite.sh @@ -4,25 +4,38 @@ # negatives, the restart budget, and the PSA client lifecycle, each built and # flashed to the board on the host that owns its MCU-Link. Without a board # detect_mcxn947.sh reports why and the suite SKIPs; it never silently passes. +# +# Its logs live under logs/, never build/: every runner cleans build/ before +# it builds, so evidence there would not survive the next scenario. One batch +# at a time owns the board, through a lock the runner shares. set -euo pipefail repo="$(cd "$(dirname "$0")/../.." && pwd)" -runner="$repo/tests/target/run_mcxn947_hardware.sh" +runner="${MCXN947_SUITE_RUNNER:-$repo/tests/target/run_mcxn947_hardware.sh}" +detect="${MCXN947_SUITE_DETECT:-$repo/tests/target/detect_mcxn947.sh}" scenarios="${WT_MCXN947_SCENARIOS:-positive sauneg periphneg restart psa}" spsdk_venv="${MCXN947_SPSDK_VENV:-$HOME/spsdk-venv}" +logs="${MCXN947_SUITE_LOGS:-$repo/logs/mcxn947-suite}" +lock="${MCXN947_HW_LOCK:-$repo/logs/mcxn947-hw.lock}" # The runner puts the SPSDK venv on PATH itself; detection needs it first. if [ -d "$spsdk_venv/bin" ]; then PATH="$spsdk_venv/bin:$PATH" export PATH fi -if ! "$repo/tests/target/detect_mcxn947.sh" >/dev/null 2>&1; then - echo "SKIP: MCXN947 hardware suite ($("$repo/tests/target/detect_mcxn947.sh" 2>&1))" +if ! "$detect" >/dev/null 2>&1; then + echo "SKIP: MCXN947 hardware suite ($("$detect" 2>&1))" exit 0 fi -logs="$repo/build/mcxn947-suite" -mkdir -p "$logs" +mkdir -p "$logs" "$(dirname "$lock")" +exec 9>"$lock" +if ! flock -n 9; then + echo "WAIT: another MCXN947 hardware batch holds $lock" + flock 9 +fi +export MCXN947_HW_LOCKED=1 + rc=0 for s in $scenarios; do echo "RUN: hardware/$s" @@ -36,5 +49,6 @@ for s in $scenarios; do rc=1 fi done +echo "LOGS: $logs" if [ "$rc" -eq 0 ]; then echo "PASS: hardware/all"; else echo "FAIL: hardware/all"; exit 1; fi diff --git a/tests/target/test_mcxn947_suite_evidence.sh b/tests/target/test_mcxn947_suite_evidence.sh new file mode 100755 index 00000000..06a86613 --- /dev/null +++ b/tests/target/test_mcxn947_suite_evidence.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# Regression check for the MCXN947 hardware suite: its logs and the runner's +# retained evidence never live under build/ (every run cleans build/ before +# it builds, so a batch would lose the previous scenario's log there), and a +# batch serializes on the board lock the runner shares. Runs without a board: +# the suite is driven with a stub runner and a stub detector. +set -euo pipefail + +repo="$(cd "$(dirname "$0")/../.." && pwd)" +suite="$repo/tests/target/run_mcxn947_suite.sh" +runner="$repo/tests/target/run_mcxn947_hardware.sh" +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT +fails=0 +check() { + if [ "$1" -eq 0 ]; then echo "PASS: $2"; else echo "FAIL: $2"; fails=$((fails + 1)); fi +} + +# The defaults, read from the scripts: nothing retained lands under build/. +check "$(grep -qE '^logs="\$\{MCXN947_SUITE_LOGS:-\$repo/logs/' "$suite"; echo $?)" \ + "the suite's logs default to logs/, not build/" +check "$(grep -qE '^work="\$\{MCXN947_WORK:-\$repo/logs/' "$runner"; echo $?)" \ + "the runner's retained evidence defaults to logs/, not build/" +check "$(! grep -qE '(logs|work|lock)="\$\{[A-Z0-9_]+:-\$repo/build' "$suite" "$runner"; echo $?)" \ + "no evidence path defaults to build/" +check "$(grep -q 'flock' "$suite" && grep -q 'flock' "$runner"; echo $?)" \ + "the suite and the runner both take the board lock" + +# A stub runner that cleans build/ the way a real run does (the suite's old +# log home), then reports; its log must survive in the suite's log dir. +stub="$tmp/runner.sh" +cat > "$stub" <<'EOF' +#!/usr/bin/env bash +rm -rf "${STUB_REPO}/build/mcxn947-suite" +echo " [check] PASS stub $1 ran with lock=${MCXN947_HW_LOCKED:-unset}" +echo "PASS: hardware/mcxn947/$1" +EOF +chmod +x "$stub" +export STUB_REPO="$repo" MCXN947_SUITE_RUNNER="$stub" MCXN947_SUITE_DETECT=/bin/true +export MCXN947_SUITE_LOGS="$tmp/logs" MCXN947_HW_LOCK="$tmp/hw.lock" +export WT_MCXN947_SCENARIOS="alpha beta" +out="$("$suite" 2>&1)"; rc=$? +check "$([ "$rc" -eq 0 ]; echo $?)" "the stub batch passes (rc $rc)" +check "$(grep -q "PASS: hardware/alpha" <<<"$out" && grep -q "PASS: hardware/beta" <<<"$out"; echo $?)" \ + "both scenarios reported" +check "$([ -s "$tmp/logs/alpha.log" ] && [ -s "$tmp/logs/beta.log" ]; echo $?)" \ + "every scenario's log survived the runner cleaning build/" +check "$(grep -q "lock=1" "$tmp/logs/beta.log"; echo $?)" \ + "the runner saw the batch's lock and did not take its own" +check "$(grep -q "^LOGS: $tmp/logs" <<<"$out"; echo $?)" "the suite names its log dir" + +# Serialization: while another batch holds the lock, a new batch waits. +( flock "$tmp/hw.lock" sleep 4 ) & +holder=$! +sleep 0.5 +rm -rf "$tmp/logs" +set +e +waited="$(timeout 2 "$suite" 2>&1)"; wrc=$? +set -e +check "$([ "$wrc" -eq 124 ]; echo $?)" "a batch waits for the lock holder (rc $wrc)" +check "$(grep -q "^WAIT:" <<<"$waited"; echo $?)" "the waiting batch says so" +check "$([ ! -e "$tmp/logs/alpha.log" ]; echo $?)" "the waiting batch ran no scenario" +wait "$holder" +"$suite" >/dev/null 2>&1 +check "$([ -s "$tmp/logs/beta.log" ]; echo $?)" "the batch runs once the lock is released" + +echo "mcxn947 suite evidence: $fails failures" +[ "$fails" -eq 0 ] From f838154a0d80e29dcd699cc23e6079f5313da4f1 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Mon, 26 Oct 2026 15:38:32 +0000 Subject: [PATCH 22/45] Retain each MCXN947 hardware run's console and dumps by scenario and engine --- docs/MCXN947-Guide.md | 8 +++-- docs/Testing.md | 2 +- tests/target/run_mcxn947_hardware.sh | 39 ++++++++++++++++----- tests/target/test_mcxn947_suite_evidence.sh | 2 ++ 4 files changed, 38 insertions(+), 13 deletions(-) diff --git a/docs/MCXN947-Guide.md b/docs/MCXN947-Guide.md index d9c88781..c0e9e047 100644 --- a/docs/MCXN947-Guide.md +++ b/docs/MCXN947-Guide.md @@ -299,9 +299,11 @@ compares it, and asserts the result over SWD: - for `psa`, each PSA guest's mailbox stage bits (TEE, crypto, ITS, PS, key-ops, SHA-256 KAT, COSE attestation, done). -The console capture, the readback and the wolfBoot build log are kept under -`logs/mcxn947-hw/`, the suite's per-scenario logs under `logs/mcxn947-suite/`: -outside `build/`, which every run cleans before it builds. +Each run's console capture, image readbacks and an SWD dump of every word +the checks read are kept under `logs/mcxn947-hw/-/` +(`console.log`, `readback-*.bin`, `swd.txt`), the suite's per-scenario logs +under `logs/mcxn947-suite/`: outside `build/`, which every run cleans before +it builds, and never overwritten by another scenario or engine. Two board facts the runner encodes: the pack's `reset halt` leaves a DWT comparator and the debug fault status armed, so the core is released only diff --git a/docs/Testing.md b/docs/Testing.md index 35c3e864..177a9e34 100644 --- a/docs/Testing.md +++ b/docs/Testing.md @@ -438,7 +438,7 @@ MCXN947 Guide lists what each scenario asserts). The two guests share one LPUART, so their console lines interleave on silicon; the console is captured as evidence and the assertions are the mailboxes and the monitor's counters. The suite's logs (`logs/mcxn947-suite/`) and the runner's retained evidence -(`logs/mcxn947-hw/`: per-scenario console, readback, wolfBoot build log) +(`logs/mcxn947-hw/-/`: console, readbacks, SWD dump) live outside `build/`, which every run cleans before it builds, and one batch at a time owns the board through `logs/mcxn947-hw.lock` (`flock`, shared by the runner); `tests/target/test_mcxn947_suite_evidence.sh` checks both under diff --git a/tests/target/run_mcxn947_hardware.sh b/tests/target/run_mcxn947_hardware.sh index cc6a9bcc..4522b348 100755 --- a/tests/target/run_mcxn947_hardware.sh +++ b/tests/target/run_mcxn947_hardware.sh @@ -40,6 +40,9 @@ repo="$(cd "$here/../.." && pwd)" # build/, which every run cleans before it builds. work="${MCXN947_WORK:-$repo/logs/mcxn947-hw}" lock="${MCXN947_HW_LOCK:-$repo/logs/mcxn947-hw.lock}" +engine="${WT_ENGINE:-native}" +# Each run's console, readbacks and SWD dump, kept per scenario and engine. +evidence="$work/$scenario-$engine" target="${MCXN947_TARGET:-mcxn947vdf}" probe_uid="${MCXN947_PROBE_UID:-}" console="${MCXN947_CONSOLE:-}" @@ -173,7 +176,7 @@ clear_mailboxes() { release_core() { local n stty -F "$console" 115200 raw -echo -crtscts -ixon 2>/dev/null || true - ( timeout "$console_seconds" cat "$console" > "$work/console-$scenario.log" 2>/dev/null ) & + ( timeout "$console_seconds" cat "$console" > "$evidence/console.log" 2>/dev/null ) & sleep 0.5 timeout 60 pyocd cmd -u "$probe_uid" -t "$target" -O resume_on_disconnect=false \ -O reset_type=hw -c "reset halt" -c "write32 0xE0001028 0" \ @@ -189,12 +192,13 @@ release_core() { # Read an image back over the debug port once the chain has booted and # compare it. verify_at() { - local addr="$1" image="$2" size + local addr="$1" image="$2" size back size="$(wc -c < "$image" | tr -d ' ')" + back="$evidence/readback-$(basename "$image")" timeout 120 pyocd cmd -u "$probe_uid" -t cortex_m \ - -c "savemem $addr $size $work/readback.bin" >/dev/null 2>&1 || \ + -c "savemem $addr $size $back" >/dev/null 2>&1 || \ fail "readback of $addr failed" - cmp -s "$work/readback.bin" "$image" || \ + cmp -s "$back" "$image" || \ fail "flash verify mismatch at $addr ($(basename "$image"))" log " [flash] verified $(basename "$image") @ $addr" } @@ -230,8 +234,8 @@ run_chain() { # boot base + 0x400 and is signed with a matching header. Exported so the # secure image and the guest CMSE import library agree. export WT_SECURE_IMAGE_HEADER_SIZE=0x400 - rm -rf "$repo/build" - mkdir -p "$work" + rm -rf "$repo/build" "$evidence" + mkdir -p "$evidence" stage "build wolfTrust secure image + CMSE import library (${WT_ENGINE:-native})" make -s -C "$repo" TARGET=mcxn947 secure-image TOOLPREFIX=arm-none-eabi- @@ -267,7 +271,24 @@ run_chain() { verify_at "$secure_flash_addr" "$repo/build/wolftrust_v1_signed.bin" verify_at "$guest0_flash_addr" "$guest_build/guest0.bin" verify_at "$guest1_flash_addr" "$guest_build/guest1.bin" - log " [console] $work/console-$scenario.log" + dump_evidence + log " [evidence] $evidence (console.log, readback-*.bin, swd.txt)" +} + +# The words every check reads, kept as text beside the console so a verdict +# can be re-read without the board. +dump_evidence() { + { + echo "scenario=$scenario engine=$engine image=$(sha256sum "$repo/build/wolftrust_v1_signed.bin" | cut -c1-16)" + dap -c "read32 $guest0_ram 48" -c "read32 $guest1_ram 48" -c "read32 $probe_addr 4" \ + -c "read32 $(elf_sym g_wt_launch_verified_mask)" -c "read32 $(elf_sym g_wt_launch_refused_mask)" \ + -c "read32 $(elf_sym g_wt_restart_events)" -c "read32 $(elf_sym g_wt_quarantine_events)" \ + -c "read32 $(elf_sym g_wt_rom_flash_last_status)" -c "read32 $(elf_sym g_wt_flash_last_error)" \ + -c "read32 $(elf_sym g_wt_els_errors) 12" -c "read32 $(elf_sym g_wt_boot_handoff) 56" \ + -c "read32 $hsm_nvm_addr 32" -c "read32 $((hsm_nvm_addr + flash_sector)) 16" \ + -c "read32 $handoff_addr 16" -c "read32 $syscon_lc_state" \ + -c halt -c "reg pc" -c "reg xpsr" -c go + } > "$evidence/swd.txt" 2>&1 || true } # One 32-bit word at base+offset over SWD, as eight lowercase hex digits. @@ -356,7 +377,7 @@ check_secure_side() { else log " [lifecycle] handoff lifecycle 0x$handed" fi - check "$(grep -q "Verifying signature...done" "$work/console-$scenario.log"; echo $?)" \ + check "$(grep -q "Verifying signature...done" "$evidence/console.log"; echo $?)" \ "wolfBoot verified the wolfTrust image signature (console)" } @@ -417,7 +438,7 @@ sauneg) periphneg) run_chain "$baremetal_dir" "WT_PERIPH_NEG_PROBE=1" check_guest0_quarantined - check "$(! grep -q "periph probe LEAKED" "$work/console-$scenario.log"; echo $?)" \ + check "$(! grep -q "periph probe LEAKED" "$evidence/console.log"; echo $?)" \ "no NS read of the SPM's ELS ever returned (console)" check_secure_side log "PASS: hardware/mcxn947/$scenario" diff --git a/tests/target/test_mcxn947_suite_evidence.sh b/tests/target/test_mcxn947_suite_evidence.sh index 06a86613..97dcd39a 100755 --- a/tests/target/test_mcxn947_suite_evidence.sh +++ b/tests/target/test_mcxn947_suite_evidence.sh @@ -25,6 +25,8 @@ check "$(! grep -qE '(logs|work|lock)="\$\{[A-Z0-9_]+:-\$repo/build' "$suite" "$ "no evidence path defaults to build/" check "$(grep -q 'flock' "$suite" && grep -q 'flock' "$runner"; echo $?)" \ "the suite and the runner both take the board lock" +check "$(grep -qE '^evidence="\$work/\$scenario-\$engine"' "$runner"; echo $?)" \ + "the runner keeps each run's console and dumps by scenario and engine" # A stub runner that cleans build/ the way a real run does (the suite's old # log home), then reports; its log must survive in the suite's log dir. From 56c8c6aed3a6c4b613923d8194a01ecdc9f78e09 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Mon, 26 Oct 2026 15:42:21 +0000 Subject: [PATCH 23/45] State why confboot is not claimed on the MCXN947 model --- docs/MCXN947-Guide.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/MCXN947-Guide.md b/docs/MCXN947-Guide.md index c0e9e047..cafe5738 100644 --- a/docs/MCXN947-Guide.md +++ b/docs/MCXN947-Guide.md @@ -38,10 +38,14 @@ the part; this port rehearses life cycles and writes none of them. matrix lists for this port (`tests/target/lib/scenario_matrix.py`: the whole MIMXRT700 set, every isolation level 3 scenario included) passes on the model on both crypto engines. The one MIMXRT700 scenario not listed is - `confboot`: the Arm FF-M IPC suite did not reach its exit breakpoint within - the 900 s budget on this model, while the dev_apis storage, crypto and - attestation suites and the vault recovery runs do; it is being run again - with a longer budget before it is claimed. + `confboot`: on the M33MU commit this port pins (the one with the MCXN947 + RAM attribution fix), the Arm FF-M IPC suite stops in test i002 check 5, + the Non-secure val guest parked in its HardFault handler with an + undefined-instruction UsageFault right after `psa_connect` returns, and + the MIMXRT700 chain does the same on that emulator commit while it passes + on the older one its runner pins; the dev_apis storage, crypto and + attestation suites and the vault recovery runs complete on both. It is an + emulator regression to bisect upstream, not a port claim. - **Not validated:** any guest flash fence and any life cycle write. The TRDC and the AHB secure controller are left as the first loader leaves them. See [Open items](#open-items). From dadfa358b0eb74bd1a8b86c211eb3fa176d372a5 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Mon, 26 Oct 2026 16:06:13 +0000 Subject: [PATCH 24/45] Flash the MCXN947 board inside the park session --- docs/MCXN947-Guide.md | 13 +++--- tests/target/run_mcxn947_hardware.sh | 59 +++++++++++++++------------- 2 files changed, 40 insertions(+), 32 deletions(-) diff --git a/docs/MCXN947-Guide.md b/docs/MCXN947-Guide.md index cafe5738..a62578a0 100644 --- a/docs/MCXN947-Guide.md +++ b/docs/MCXN947-Guide.md @@ -309,11 +309,14 @@ the checks read are kept under `logs/mcxn947-hw/-/` under `logs/mcxn947-suite/`: outside `build/`, which every run cleans before it builds, and never overwritten by another scenario or engine. -Two board facts the runner encodes: the pack's `reset halt` leaves a DWT -comparator and the debug fault status armed, so the core is released only -after both are cleared; and the MCU-Link forwards early-boot UART only while -the debug link stays active, so the console capture runs alongside spaced -reads. The two guests share LPUART4, so their console lines interleave on +Three board facts the runner encodes: the device pack resets the chip on +every pyOCD connect, so the park and the flash, erase and SRAM writes that +depend on it share one session (a second session finds the chain running and +pyOCD's flash routine then races wolfTrust's tick); the pack's `reset halt` +leaves a DWT comparator and the debug fault status armed, so the core is +released only after both are cleared; and the MCU-Link forwards early-boot +UART only while the debug link stays active, so the console capture runs +alongside spaced reads. The two guests share LPUART4, so their console lines interleave on silicon; the assertions are the mailboxes and counters, the console is evidence. diff --git a/tests/target/run_mcxn947_hardware.sh b/tests/target/run_mcxn947_hardware.sh index 4522b348..1fc8f415 100755 --- a/tests/target/run_mcxn947_hardware.sh +++ b/tests/target/run_mcxn947_hardware.sh @@ -55,8 +55,9 @@ wolfboot_flash_addr=0x00000000 secure_flash_addr=0x00040000 guest0_flash_addr=0x00080000 guest1_flash_addr=0x00100000 -update_erase=0x00180000-0x001C2000 -nvm_erase=0x001E0000-0x001E6000 +# 8 KiB sectors: update and swap 0x180000-0x1C2000, NVM 0x1E0000-0x1E6000. +update_erase="0x00180000 33" +nvm_erase="0x001E0000 3" hsm_nvm_addr=0x001E0000 flash_sector=0x2000 guest0_ram=0x20000000 @@ -122,34 +123,28 @@ dap() { grep -viE "rom table|APB-AP|coresight|cidr" } -# Park the core before wolfTrust owns it: pyOCD's flash algorithm runs in RAM -# and faults under wolfTrust's Secure MPU whitelist, so the halt is followed -# by disabling the MPU explicitly. -park_core() { - timeout 60 pyocd cmd -u "$probe_uid" -t "$target" -O resume_on_disconnect=false \ - -O reset_type=hw -c "reset halt" -c "write32 0xE000ED94 0" \ - >/dev/null 2>&1 || fail "could not park the core" -} +# pyOCD's flash routine runs in RAM and faults under wolfTrust's Secure MPU +# whitelist, so the core is parked and the MPU disabled first. The pack resets +# the chip on every connect, so the park and every step that depends on it +# share one pyOCD session; a second session would find the chain running. +park_cmds=(-O reset_type=hw -c "reset halt" -c "write32 0xE000ED94 0") +board_cmds=() # A failed or partial flash must stop the run: a stale image either boots old # code or fails its pinned measurement and looks like a port bug. flash_at() { - local addr="$1" image="$2" out + local addr="$1" image="$2" [ -s "$image" ] || fail "flash image missing: $image" - park_core - if ! out="$(timeout 300 pyocd flash -u "$probe_uid" -t "$target" \ - -O resume_on_disconnect=false --no-reset -a "$addr" -e sector "$image" 2>&1)"; then - printf '%s\n' "$out" | tail -6 - fail "flash of $(basename "$image") at $addr failed" - fi + board_cmds+=(-c "load $image $addr") } # The board keeps the update, swap and NVM sectors across runs where the # emulator starts fresh; erasing them puts every run on a blank vault. erase_ranges() { - park_core - timeout 180 pyocd erase -u "$probe_uid" -t "$target" -O resume_on_disconnect=false \ - -s "$@" >/dev/null 2>&1 || fail "erase of $* failed" + local range + for range in "$@"; do + board_cmds+=(-c "erase $range") + done } # SRAM survives a warm reset, so the last run's mailboxes, sentinel and launch @@ -159,13 +154,23 @@ clear_mailboxes() { verified="$(elf_sym g_wt_launch_verified_mask)" refused="$(elf_sym g_wt_launch_refused_mask)" [ -n "$verified" ] && [ -n "$refused" ] || fail "launch masks not found in wolftrust.elf" - park_core - timeout 60 pyocd cmd -u "$probe_uid" -t "$target" -O resume_on_disconnect=false \ - -c "write32 $guest0_ram 0 0 0 0 0 0 0 0 0 0 0 0" \ + board_cmds+=(-c "write32 $guest0_ram 0 0 0 0 0 0 0 0 0 0 0 0" \ -c "write32 $guest1_ram 0 0 0 0 0 0 0 0 0 0 0 0" \ -c "write32 $probe_addr 0" \ - -c "write32 $verified 0" -c "write32 $refused 0" >/dev/null 2>&1 || \ - fail "could not clear the guest mailboxes" + -c "write32 $verified 0" -c "write32 $refused 0") +} + +# The commander exits 0 whatever a command did, so its output is the verdict. +program_board() { + local out + [ "${#board_cmds[@]}" -gt 0 ] || fail "nothing to program" + out="$(timeout 900 pyocd cmd -u "$probe_uid" -t "$target" -O resume_on_disconnect=false \ + "${park_cmds[@]}" "${board_cmds[@]}" 2>&1)" || true + if printf '%s\n' "$out" | grep -qiE 'error|exception|not halted|failure|not within|not in flash'; then + printf '%s\n' "$out" | grep -iE 'error|exception|not halted|failure|not within|not in flash' | tail -6 + fail "programming the board failed" + fi + board_cmds=() } # Boot from the parked state: the pack's halt leaves a DWT comparator and the @@ -256,14 +261,14 @@ run_chain() { "$repo/build/wolftrust.bin" \ "$wolfboot_dir/wolfboot_signing_private_key.der" 1 - stage "flash the chain" + stage "flash the chain, erase the update, swap and NVM sectors, clear SRAM" flash_at "$wolfboot_flash_addr" "$wolfboot_dir/wolfboot.bin" flash_at "$secure_flash_addr" "$repo/build/wolftrust_v1_signed.bin" flash_at "$guest0_flash_addr" "$guest_build/guest0.bin" flash_at "$guest1_flash_addr" "$guest_build/guest1.bin" - stage "erase the update, swap and NVM sectors so the run starts from a fresh vault" erase_ranges "$update_erase" "$nvm_erase" clear_mailboxes + program_board stage "boot the chain, reading the console for ${console_seconds}s" release_core From e967c0422a40b43a32fab285b7e8d7b100bb7427 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Mon, 26 Oct 2026 17:07:13 +0000 Subject: [PATCH 25/45] Bind the MCXN947 rehearsal to its probe and preview the lock from the vendor tools --- tests/host/mcxn947_lifecycle/main.c | 37 ++++ .../provisioning/provisioning_ctrl_mcxn947.sh | 198 +++++++++++++++--- .../provisioning/test_provisioning_gates.sh | 102 ++++++++- 3 files changed, 294 insertions(+), 43 deletions(-) diff --git a/tests/host/mcxn947_lifecycle/main.c b/tests/host/mcxn947_lifecycle/main.c index a036c50c..723925e0 100644 --- a/tests/host/mcxn947_lifecycle/main.c +++ b/tests/host/mcxn947_lifecycle/main.c @@ -124,11 +124,48 @@ static void test_debug_status(void) MCXN_LC_OEM_LOCKED, DAUTH_CLOSED), 0x3000u); } +/* The OTP fuse map SPSDK carries for the part (fuses.json, register + * LIFE_CYCLE, index 1): LIFECYCLE[7:0] at bit 0 and its duplicate at bit 24 + * use the same codes the ROM reflects into SYSCON, and every ladder step only + * sets fuse bits, which is what makes it one way. */ +#define FUSE_LC_DEVELOP 3u +#define FUSE_LC_DEVELOP2 7u +#define FUSE_LC_IN_FIELD 15u +#define FUSE_LC_IN_FIELD_LOCKED 207u +#define FUSE_LC_FIELD_RETURN_OEM 31u +#define FUSE_LC_WORD(lc) ((uint32_t)(lc) | ((uint32_t)(lc) << 24)) + +static void test_fuse_map(void) +{ + static const uint32_t ladder[][2] = { + { MCXN_LC_OEM_OPEN, MCXN_LC_OEM_SECURE_WORLD_CLOSED }, + { MCXN_LC_OEM_SECURE_WORLD_CLOSED, MCXN_LC_OEM_CLOSED }, + { MCXN_LC_OEM_CLOSED, MCXN_LC_OEM_LOCKED }, + { MCXN_LC_OEM_CLOSED, MCXN_LC_OEM_FIELD_RETURN }, + }; + size_t i; + + EXPECT_LC(MCXN_LC_OEM_OPEN, FUSE_LC_DEVELOP); + EXPECT_LC(MCXN_LC_OEM_SECURE_WORLD_CLOSED, FUSE_LC_DEVELOP2); + EXPECT_LC(MCXN_LC_OEM_CLOSED, FUSE_LC_IN_FIELD); + EXPECT_LC(MCXN_LC_OEM_LOCKED, FUSE_LC_IN_FIELD_LOCKED); + EXPECT_LC(MCXN_LC_OEM_FIELD_RETURN, FUSE_LC_FIELD_RETURN_OEM); + EXPECT_LC(FUSE_LC_WORD(MCXN_LC_OEM_SECURE_WORLD_CLOSED), 0x07000007u); + EXPECT_LC(FUSE_LC_WORD(MCXN_LC_OEM_CLOSED), 0x0F00000Fu); + for (i = 0; i < sizeof(ladder) / sizeof(ladder[0]); i++) { + EXPECT_LC(FUSE_LC_WORD(ladder[i][0]) & ~FUSE_LC_WORD(ladder[i][1]), + 0u); + EXPECT_LC(mcxn_lc_to_psa_lifecycle(ladder[i][1], ladder[i][1]) != 0u, + 1u); + } +} + int main(void) { test_state_table(); test_redundancy(); test_debug_status(); + test_fuse_map(); printf("%s: %d checks, %d failures\n", (failures == 0) ? "PASS" : "FAIL", checks, failures); return (failures == 0) ? 0 : 1; diff --git a/tests/target/provisioning/provisioning_ctrl_mcxn947.sh b/tests/target/provisioning/provisioning_ctrl_mcxn947.sh index d8525ed8..0629372a 100644 --- a/tests/target/provisioning/provisioning_ctrl_mcxn947.sh +++ b/tests/target/provisioning/provisioning_ctrl_mcxn947.sh @@ -19,8 +19,10 @@ # along with this program; if not, see . # MCXN947 (FRDM-MCXN947) port for provisioning_ctrl.sh: the OTP life cycle -# the ROM reflects into SYSCON, rehearsed in the boot handoff record; no -# permanent write is implemented. Sourced, never run. See docs/MCXN947-Guide.md. +# the ROM reflects into SYSCON, rehearsed in the boot handoff record; the +# lock is previewed from the vendor tools (SPSDK pfr and nxpfuses) and +# refused, hard-wired off in this phase. Sourced, never run. See +# docs/MCXN947-Guide.md. # shellcheck disable=SC2034,SC2154 # PORT_* are read, and $cur/$PENDING set, by the main script # A port file only works inside provisioning_ctrl.sh, which holds the gates. @@ -31,7 +33,7 @@ fi PORT_NAME="MCXN947" target_dir="$here/.." -pyocd_target="${MCXN947_TARGET:-mcxn947}" +pyocd_target="${MCXN947_TARGET:-mcxn947vdf}" spsdk_venv="${MCXN947_SPSDK_VENV:-$HOME/spsdk-venv}" # Set here, not in a helper: the controller runs many port calls in subshells. [ ! -d "$spsdk_venv/bin" ] || PATH="$spsdk_venv/bin:$PATH" @@ -48,10 +50,18 @@ case "$guest_mask" in *) refuse "MCXN947_GUEST_MASK must be 0x1, 0x2, or 0x3 (both guests), not '$guest_mask'." ;; esac elf="$repo/build/wolftrust.elf" +wolfboot_dir="${MCXN947_WOLFBOOT_DIR:-$HOME/.cache/wolftrust/wolfboot-mcxn947}" +# The blhost connection of the part in ISP mode, named only in the preview. +isp="${MCXN947_ISP:-}" +# The life cycle write stays hard-wired off in this phase: the lock previews +# the exact vendor write and refuses it whatever the caller supplies. +MCXN947_LOCK_WRITE_ENABLED=0 # SYSCON life cycle state the ROM reflects from OTP (read-only), its # duplicate, the debug authentication status, the device UUID (IPC1), the -# BOOT partition's reset vector, and the handoff record. +# BOOT partition's reset vector, the handoff record, and the PFR pages +# (SPSDK device database: CMPA 0x01004000, CFPA 0x01000000, 512 bytes; the +# CFPA ping and pong copies follow it). LC_STATE=0x50000980 LC_STATE_DP=0x50000984 DAUTHSTATUS=0xE000EFB8 @@ -59,11 +69,19 @@ UUID_BASE=0x01100000 WOLFTRUST_RESET_VECTOR=0x10040404 HANDOFF=0x30030000 LC_OEM_OPEN=0x03 +CMPA_ADDR=0x01004000 +CFPA_SCRATCH_ADDR=0x01000000 +CFPA_PING_ADDR=0x01002000 +CFPA_PONG_ADDR=0x01003000 +PFR_PAGE_SIZE=512 +# The OTP fuse SPSDK's fuse map names LIFE_CYCLE (index 1): LIFECYCLE[7:0] +# and its duplicate LIFECYCLE_DP[7:0] carry the same code the ROM reflects. +FUSE_LIFE_CYCLE=1 port_ladder() { cat <<'EOF' 0x03 oem-open no no permanent - -0x07 oem-secure-world yes no permanent 0x03 +0x07 oem-secure-world yes yes permanent 0x03 0x0F oem-closed yes no permanent 0x07 0xCF oem-locked yes no permanent 0x0F 0x1F oem-field-return yes no permanent 0x0F @@ -72,10 +90,18 @@ EOF port_usage_extra() { cat <<'EOF' verify-wrp report the guest flash fence (none on this port yet) + verify-pages re-read the CMPA and CFPA pages and compare them with the last lock preview EOF } lc_hex() { printf '0x%02X' $(( $1 & 0xFF )); } +# SPSDK's fuse map name for a life cycle code (nxpfuses LIFECYCLE[7:0]). +fuse_lc_name() { + case "$1" in + 0x03) echo DEVELOP ;; 0x07) echo DEVELOP2 ;; 0x0F) echo IN_FIELD ;; + 0xCF) echo IN_FIELD_LOCKED ;; 0x1F) echo FIELD_RETURN_OEM ;; + esac +} ensure_pyocd() { if ! command -v pyocd >/dev/null 2>&1; then [ -x "$spsdk_venv/bin/pyocd" ] || fail "tools" "pyocd not found (set MCXN947_SPSDK_VENV)" @@ -83,13 +109,46 @@ ensure_pyocd() { export PATH fi } +ensure_spsdk() { + ensure_pyocd + command -v pfr >/dev/null 2>&1 && command -v nxpfuses >/dev/null 2>&1 || + fail "tools" "SPSDK pfr and nxpfuses not found (set MCXN947_SPSDK_VENV)" +} +# The probe this host names, else the one attached debug probe; the FRDM +# board's MCU-Link is soldered on. Resolved once, outside any subshell, so a +# missing id refuses loudly instead of leaving a read empty. +probe_uid() { + local ids + if [ -n "${MCXN947_PROBE_UID:-}" ]; then echo "$MCXN947_PROBE_UID"; return 0; fi + ids="$(timeout 30 pyocd list 2>/dev/null | awk '$1 ~ /^[0-9]+$/ { print $(NF-1) }')" + [ "$(printf '%s\n' "$ids" | grep -c .)" = "1" ] && echo "$ids" +} +ensure_probe() { + ensure_pyocd + [ -n "${PROBE:-}" ] && return 0 + PROBE="$(probe_uid)" || + refuse "this host has more than one debug probe: set MCXN947_PROBE_UID to the FRDM-MCXN947 MCU-Link's unique id (pyocd list)." +} +# pyocd bound to that probe: . +pyocd_probe() { + local secs="$1" + shift + [ -n "${PROBE:-}" ] || refuse "no debug probe bound (ensure_probe was not called)." + timeout "$secs" pyocd "$@" -u "$PROBE" +} # Words over SWD with the generic attach, which never resets the chip. read_words() { local -a cmds=() local a for a in "$@"; do cmds+=(-c "read32 $a"); done - timeout 60 pyocd cmd -t cortex_m "${cmds[@]}" 2>&1 | awk '/^[0-9a-f]+:/ { print $2 }' + pyocd_probe 60 cmd -t cortex_m "${cmds[@]}" 2>&1 | awk '/^[0-9a-f]+:/ { print $2 }' +} +# One PFR page (512 bytes) over SWD into a file; the IFR is memory mapped. +read_page() { + pyocd_probe 60 cmd -t cortex_m -c "savemem $1 $PFR_PAGE_SIZE $2" >/dev/null 2>&1 && + [ "$(wc -c < "$2" | tr -d ' ')" = "$PFR_PAGE_SIZE" ] } +page_digest() { sha256 < "$1" | cut -c1-64; } psa_name() { case "$1" in 00001000) echo "ASSEMBLY_AND_TEST" ;; 00002000) echo "PSA_ROT_PROVISIONING" ;; @@ -134,10 +193,11 @@ handoff_lifecycle() { read_words "$sym" } # The four images the chain flashes, as "address file" lines (physical -# offsets; wolfBoot at 0, the BOOT partition and both guest windows). +# offsets; wolfBoot at 0, the BOOT partition and both guest windows), the +# ones run_mcxn947_hardware.sh builds and flashes. flashed_images() { printf '%s %s\n' \ - 0x00000000 "${MCXN947_WORK:-$repo/build/mcxn947}/flash_wolfboot.bin" \ + 0x00000000 "$wolfboot_dir/wolfboot.bin" \ 0x00040000 "$repo/build/wolftrust_v1_signed.bin" \ 0x00080000 "$repo/tests/firmware/mcxn947-baremetal/build/guest0.bin" \ 0x00100000 "$repo/tests/firmware/mcxn947-baremetal/build/guest1.bin" @@ -146,7 +206,7 @@ port_image_digest() { framed_digest; } images_on_device() { local a f while read -r a f; do - timeout 120 pyocd cmd -t cortex_m \ + pyocd_probe 120 cmd -t cortex_m \ -c "savemem $a $(wc -c < "$f" | tr -d ' ') $state_dir/readback.bin" \ >/dev/null 2>&1 && cmp -s "$state_dir/readback.bin" "$f" || return 1 done < <(flashed_images) @@ -158,12 +218,6 @@ chip_uuid() { [ ${#w} -eq 32 ] && [ "$w" != "00000000000000000000000000000000" ] && [ "$w" != "ffffffffffffffffffffffffffffffff" ] && echo "$w" } -# The one attached debug probe; the FRDM board's MCU-Link is soldered on. -probe_uid() { - local ids - ids="$(timeout 30 pyocd list 2>/dev/null | awk '$1 ~ /^[0-9]+$/ { print $(NF-1) }')" - [ "$(printf '%s\n' "$ids" | grep -c .)" = "1" ] && echo "$ids" -} # The MCXN947 has no guest flash fence yet: the array is written only through # the ROM API from the Secure side, and no hardware write fence is claimed. fence_line() { @@ -173,12 +227,15 @@ fence_line() { port_status() { local lc lcd dauth hl vm rf uuid - ensure_pyocd + ensure_probe read -r lc lcd dauth < <(read_words "$LC_STATE" "$LC_STATE_DP" "$DAUTHSTATUS" | tr '\n' ' '; echo) + [ -n "${lc:-}" ] && [ -n "${lcd:-}" ] || fail "status" "cannot read the life cycle over SWD through probe $PROBE" + echo "debug probe $PROBE" echo "OTP life cycle LC_STATE=0x${lc: -2} ($(state_name "$(lc_hex "0x$lc")" || true)) LC_STATE_DP=0x${lcd: -2}" echo "DAUTHSTATUS 0x$dauth" echo "device UUID $(chip_uuid || echo unreadable)" echo "guest fence $(fence_line || true)" + echo "lock write hard-wired off (MCXN947_LOCK_WRITE_ENABLED=$MCXN947_LOCK_WRITE_ENABLED): lock previews only" if hl="$(handoff_lifecycle)"; then echo "wolfTrust saw 0x$hl ($(psa_name "$hl"))"; fi if [ -s "$elf" ]; then read -r vm rf < <(read_words "$(elf_sym g_wt_launch_verified_mask)" \ @@ -188,11 +245,11 @@ port_status() { } port_discover() { local lc lcd dauth hl uuid - ensure_pyocd + ensure_probe mkdir -p "$state_dir" rm -f "$state_dir/discovery" "$state_dir/regress-ok" read -r lc lcd dauth < <(read_words "$LC_STATE" "$LC_STATE_DP" "$DAUTHSTATUS" | tr '\n' ' '; echo) - [ -n "${lc:-}" ] && [ -n "${lcd:-}" ] || fail "discover" "cannot read the life cycle over SWD" + [ -n "${lc:-}" ] && [ -n "${lcd:-}" ] || fail "discover" "cannot read the life cycle over SWD through probe $PROBE" [ "$(lc_hex "0x$lc")" = "$(lc_hex "0x$lcd")" ] || fail "discover" "life cycle copies disagree (LC 0x$lc, DP 0x$lcd)" [ $((0x$lc & 0xFFFFFF00)) -eq 0 ] && [ $((0x$lcd & 0xFFFFFF00)) -eq 0 ] || fail "discover" "life cycle words 0x$lc/0x$lcd carry bits above the state byte" @@ -200,16 +257,19 @@ port_discover() { 0x03|0x07|0x0F) ;; *) fail "discover" "life cycle $(lc_hex "0x$lc") has no further rehearsal step" ;; esac - pass "life cycle is $(label "$(lc_hex "0x$lc")") and its duplicate agrees" + pass "life cycle is $(label "$(lc_hex "0x$lc")") and its duplicate agrees (probe $PROBE)" uuid="$(chip_uuid)" || fail "discover" "cannot read the device UUID at $UUID_BASE" pass "device UUID $uuid" hl="$(handoff_lifecycle)" || fail "discover" "no readable boot handoff: boot the wolfTrust chain first" pass "the boot handoff life cycle is readable (0x$hl, $(psa_name "$hl"))" - printf 'LC=0x%s DP=0x%s DAUTH=0x%s UUID=%s\n' "$lc" "$lcd" "$dauth" "$uuid" > "$state_dir/discovery" + printf 'LC=0x%s DP=0x%s DAUTH=0x%s UUID=%s PROBE=%s\n' "$lc" "$lcd" "$dauth" "$uuid" "$PROBE" > "$state_dir/discovery" echo "PASS: discovery stamped ($state_dir/discovery)" } +# The production images, through the hardware runner bound to this probe. port_restore() { - fail "restore" "no MCXN947 hardware runner yet: flash wolfBoot, the signed wolfTrust image and both guests by hand (see the MCXN947 Guide), then run 'discover'" + ensure_probe + MCXN947_PROBE_UID="$PROBE" "$target_dir/run_mcxn947_hardware.sh" positive || + fail "restore" "the hardware runner did not pass; see its output" } port_extra() { local line @@ -217,6 +277,9 @@ port_extra() { verify-wrp) line="$(fence_line)" || fail "verify-wrp" "$line" pass "guest fence $line" ;; + verify-pages) + ensure_probe + verify_pages ;; provision-da|burn) refuse "'$1' would program OTP: this port rehearses life cycles only and writes none of them; see the MCXN947 Guide." ;; set-perimeter|set-wrp|clear-wrp) @@ -235,15 +298,15 @@ port_advance_check() { # the target state's, and resume. The fuses and SYSCON are untouched. port_advance() { local entry vm rf psa - ensure_pyocd + ensure_probe vm="$(elf_sym g_wt_launch_verified_mask)"; rf="$(elf_sym g_wt_launch_refused_mask)" [ -n "$vm" ] && [ -n "$rf" ] || fail "advance" "launch masks not found in $elf" entry="$(read_words "$WOLFTRUST_RESET_VECTOR")" [ -n "$entry" ] && [ "$entry" != "00000000" ] && [ "$entry" != "ffffffff" ] || fail "advance" "no wolfTrust reset vector at $WOLFTRUST_RESET_VECTOR (flash the chain first)" psa="$(mock_psa "$1")" - echo "ADVANCING the boot handoff life cycle to $psa for $1 ($(state_name "$1")); regress or any reset undoes it" - "$spsdk_py" - "0x$entry" "$psa" "$HANDOFF" "$pyocd_target" "$vm" "$rf" <<'PYEOF' + echo "ADVANCING the boot handoff life cycle to $psa for $1 ($(state_name "$1")) through probe $PROBE; regress or any reset undoes it" + "$spsdk_py" - "0x$entry" "$psa" "$HANDOFF" "$pyocd_target" "$vm" "$rf" "$PROBE" <<'PYEOF' import sys import time from pyocd.core.helpers import ConnectHelper @@ -252,6 +315,7 @@ from pyocd.core.target import Target HANDOFF_MAGIC = 0x5742484F entry, psa, handoff = (int(a, 0) for a in sys.argv[1:4]) with ConnectHelper.session_with_chosen_probe( + unique_id=sys.argv[7], options={"target_override": sys.argv[4], "resume_on_disconnect": True, "reset_type": "hw"}) as s: t = s.target @@ -283,9 +347,10 @@ PYEOF sleep 3 } # The rehearsal evidence: the life cycle wolfTrust saw, every guest launched -# verified, the images read back, and the device UUID. +# verified, the images read back, the device UUID, and the probe. port_booted() { local hl want vm rf digest uuid + ensure_probe # advance cleared the launch masks, so both are this boot's. hl="$(handoff_lifecycle)" || { echo "no boot handoff life cycle readable after the advance"; return 1; } echo "wolfTrust saw 0x$hl ($(psa_name "$hl"))" @@ -312,12 +377,12 @@ port_booted() { images_on_device || { echo "the images on the part differ from the host build: flash the chain again"; return 1; } pass "the images on the part match the host build (${digest:0:16})" uuid="$(chip_uuid)" || { echo "cannot read the device UUID"; return 1; } - EVIDENCE="id=$uuid image=$digest fused=$(fused_lc) fence=none" + EVIDENCE="id=$uuid image=$digest fused=$(fused_lc) fence=none probe=$PROBE" } port_regress() { local fused lc lcd hl - ensure_pyocd - timeout 60 pyocd reset -t "$pyocd_target" -m hw >/dev/null 2>&1 || + ensure_probe + pyocd_probe 60 reset -t "$pyocd_target" -m hw >/dev/null 2>&1 || fail "regress" "hardware reset through pyocd failed" sleep 3 fused="$(fused_lc || echo "$LC_OEM_OPEN")" @@ -340,7 +405,7 @@ port_regress() { port_cred_fp() { echo none; } port_lock_current() { local lc lcd - ensure_pyocd + ensure_probe read -r lc lcd < <(read_words "$LC_STATE" "$LC_STATE_DP" | tr '\n' ' '; echo) [ -n "${lc:-}" ] && [ -n "${lcd:-}" ] || refuse "cannot read the life cycle over SWD." [ "$(lc_hex "0x$lc")" = "$(lc_hex "0x$lcd")" ] || refuse "the life cycle copies disagree (LC 0x$lc, DP 0x$lcd)." @@ -350,17 +415,82 @@ port_lock_identity() { chip_uuid || true; } port_rehearsal_for() { echo "$1"; } port_record_ok() { local rfused + ensure_probe rfused="$(field fused "$1")" [ -n "$rfused" ] && [ $((rfused & ~cur & 0xFF)) -eq 0 ] && [ "$(field fence "$1")" = "none" ] && + [ "$(field probe "$1")" = "$PROBE" ] && [ "$(chip_uuid || true)" = "$(field id "$1")" ] } -# No life cycle write is implemented for this part: the ladder marks every -# state unlockable, and these refuse again should the ladder ever change. + +# --- The lock preview: the live pages, parsed; the state's fuse script and +# CMPA page from the vendor tools; the diff. Nothing here writes. --- +preview_dir() { echo "$state_dir/lock-$1"; } +# Read the four PFR pages into $1/, parse each, and digest them. +read_pages() { + local d="$1" n a + mkdir -p "$d" + for n in cmpa:$CMPA_ADDR cfpa-scratch:$CFPA_SCRATCH_ADDR cfpa-ping:$CFPA_PING_ADDR cfpa-pong:$CFPA_PONG_ADDR; do + a="${n##*:}"; n="${n%%:*}" + read_page "$a" "$d/$n.bin" || return 1 + pfr parse -f mcxn947 -t "${n%%-*}" -b "$d/$n.bin" -o "$d/$n.yaml" >/dev/null 2>&1 || return 1 + printf '%s %s\n' "$(page_digest "$d/$n.bin")" "$n.bin" + done +} +# The state's exact OTP write from SPSDK's fuse map, and its CMPA page from +# the live configuration (the life cycle lives in OTP, so the page is kept). +generate_lock() { + local d="$1" state="$2" name + name="$(fuse_lc_name "$state")" + [ -n "$name" ] || return 1 + printf 'family: mcxn947\nrevision: latest\nregisters:\n LIFE_CYCLE:\n "LIFECYCLE[7:0]": %s\n "LIFECYCLE_DP[7:0]": %s\n' \ + "$name" "$name" > "$d/fuses-$state.yaml" + nxpfuses fuses-script -c "$d/fuses-$state.yaml" -o "$d/burn-$state.bls" >/dev/null 2>&1 || return 1 + grep -q "^efuse-program-once $FUSE_LIFE_CYCLE " "$d/burn-$state.bls" || return 1 + pfr export -c "$d/cmpa.yaml" -o "$d/cmpa-$state.bin" >/dev/null 2>&1 || return 1 + pfr parse -f mcxn947 -t cmpa -b "$d/cmpa-$state.bin" -o "$d/cmpa-$state.yaml" >/dev/null 2>&1 || return 1 + { diff "$d/cmpa.yaml" "$d/cmpa-$state.yaml" | grep '^[<>]' | grep -v '^. *#' || true; } > "$d/cmpa-$state.diff" +} port_ready() { - refuse "$(label "$1") cannot be locked by this port: the MCXN947 life cycle write (ROM ISP, OTP) is not validated here, so only rehearsals run; see the MCXN947 Guide." + local d digests + ensure_spsdk + ensure_probe + case "$1" in + 0x0F|0xCF|0x1F) + refuse "$(label "$1") needs the BootROM to authenticate wolfBoot (a signed image under the fused root key table hash, SEC_BOOT_EN in the CMPA), which this port does not build yet; see the MCXN947 Guide." ;; + esac + d="$(preview_dir "$1")" + digests="$(read_pages "$d")" || refuse "cannot read and parse the CMPA and CFPA pages over SWD (probe $PROBE)." + printf '%s\n' "$digests" > "$d/pages.sha256" + generate_lock "$d" "$1" || refuse "SPSDK could not generate the $(label "$1") fuse script and CMPA page." + CHECKED="$CHECKED, same debug probe $PROBE, CMPA/CFPA read and parsed ($(awk 'NR == 1 { print substr($1, 1, 16) }' "$d/pages.sha256")), fuse script and page generated in $d" +} +port_lock_plan() { + local d + d="$(preview_dir "$1")" + echo "blhost $isp batch $d/burn-$1.bls" + grep -v '^#' "$d/burn-$1.bls" | grep -v '^$' | sed 's/^/ /' + echo "pfr write: nothing (the life cycle is OTP; the CMPA re-exported from the live page differs in $(cmp -l "$d/cmpa.bin" "$d/cmpa-$1.bin" 2>/dev/null | wc -l | tr -d ' ') bytes, field diff in $d/cmpa-$1.diff)" + echo "REFUSED at the write: MCXN947_LOCK_WRITE_ENABLED=$MCXN947_LOCK_WRITE_ENABLED, hard-wired off in this port" } -port_lock_plan() { refuse "no MCXN947 life cycle write is implemented."; } port_consequence() { echo "the OTP life cycle cannot be unburned, and the part never returns to $(label "$cur")."; } -port_lock_write() { refuse "no MCXN947 life cycle write is implemented."; } +port_lock_write() { + [ "$MCXN947_LOCK_WRITE_ENABLED" = "1" ] || + refuse "the MCXN947 life cycle write is hard-wired off in this port (MCXN947_LOCK_WRITE_ENABLED=0): the preview is the whole of what this phase does, and nothing was written." + refuse "no MCXN947 life cycle write path is implemented even when enabled." +} port_lock_verify() { refuse "no MCXN947 life cycle write is implemented."; } +# The pages as they are now against the last preview's digests. +verify_pages() { + local d n now was + d="$(ls -d "$state_dir"/lock-0x* 2>/dev/null | tail -1)" + [ -n "$d" ] && [ -s "$d/pages.sha256" ] || fail "verify-pages" "no lock preview recorded yet: run 'lock ' first" + mkdir -p "$d/after" + for n in cmpa:$CMPA_ADDR cfpa-scratch:$CFPA_SCRATCH_ADDR cfpa-ping:$CFPA_PING_ADDR cfpa-pong:$CFPA_PONG_ADDR; do + read_page "${n##*:}" "$d/after/${n%%:*}.bin" || fail "verify-pages" "cannot read ${n%%:*} over SWD" + now="$(page_digest "$d/after/${n%%:*}.bin")" + was="$(awk -v f="${n%%:*}.bin" '$2 == f { print $1 }' "$d/pages.sha256")" + [ "$now" = "$was" ] || fail "verify-pages" "${n%%:*} changed since the preview (was $was, now $now)" + pass "${n%%:*} unchanged since the preview (${now:0:16})" + done +} diff --git a/tests/target/provisioning/test_provisioning_gates.sh b/tests/target/provisioning/test_provisioning_gates.sh index efc410a4..1b4455ee 100755 --- a/tests/target/provisioning/test_provisioning_gates.sh +++ b/tests/target/provisioning/test_provisioning_gates.sh @@ -93,15 +93,17 @@ esac EOF cat > "$T/venv/bin/pyocd" <> $T/pyocd_calls if [ "\$1" = cmd ]; then - # With a register file (MCXN947), answer each read32 from it; else the - # RT700 SFP dump. + # With a register file (MCXN947), answer each read32 from it and give a + # savemem a blank page; else the RT700 SFP dump. if [ -s "$T/mcxn_regs" ]; then for a in "\$@"; do case "\$a" in "read32 "*) k=\$(printf '%08x' "\${a#read32 }" 2>/dev/null || echo 0) v=\$(awk -v k="\$k" '\$1 == k { print \$2 }' "$T/mcxn_regs") echo "\$k: \${v:-00000000}" ;; + "savemem "*) set -- \$a; head -c "\$3" /dev/zero | tr '\\000' '\\377' > "\$4" ;; esac done exit 0 @@ -119,7 +121,43 @@ printf '#!/bin/sh\nexec %s "$@"\n' "$(command -v python3)" > "$T/venv/bin/python sha() { if command -v sha256sum >/dev/null 2>&1; then sha256sum; else shasum -a 256; fi; } subst() { sed "s/$1/$2/" "$3" > "$3.tmp" && mv "$3.tmp" "$3"; } have_expect=0; command -v expect >/dev/null 2>&1 && have_expect=1 -chmod +x "$T/bin/stcli" "$T/venv/bin/blhost" "$T/venv/bin/pyocd" +# SPSDK stubs for the MCXN947 lock preview: pfr parses a page into a YAML +# that names its source and exports that source back (a byte-true round +# trip); nxpfuses writes the fuse script for the named life cycle; every +# write-class command of pfr, nxpfuses and nxpdebugmbox is logged and fails. +cat > "$T/venv/bin/pfr" < "\$o" ;; + export) while [ \$# -gt 0 ]; do case "\$1" in -c) c="\$2"; shift;; -o) o="\$2"; shift;; esac; shift; done + cp "\$(sed -n 's/^source: //p' "\$c")" "\$o" ;; + get-template) while [ \$# -gt 0 ]; do case "\$1" in -o) o="\$2"; shift;; esac; shift; done + printf 'family: mcxn947\\n' > "\$o" ;; + *) echo "pfr \$sub \$*" >> $T/writes; exit 1 ;; +esac +EOF +cat > "$T/venv/bin/nxpfuses" < "\$o" ;; + *) echo "nxpfuses \$sub \$*" >> $T/writes; exit 1 ;; +esac +EOF +cat > "$T/venv/bin/nxpdebugmbox" <> $T/writes; exit 1 ;; +esac +EOF +chmod +x "$T/bin/stcli" "$T/venv/bin/blhost" "$T/venv/bin/pyocd" "$T/venv/bin/pfr" "$T/venv/bin/nxpfuses" "$T/venv/bin/nxpdebugmbox" pass=0; failn=0 check() { # check -- cmd... @@ -376,11 +414,24 @@ check "In Field refused until ROM authentication" 2 "needs the BootROM to authen nowrite echo "== MCXN947" -MX=(env TARGET=mcxn947 MCXN947_SPSDK_VENV="$T/venv" WT_PROVISION_STATE="$T/st" PATH="$T/venv/bin:$PATH" "$P") -mkdir -p "$R/build/mcxn947" "$R/tests/firmware/mcxn947-baremetal/build" -echo mwb > "$R/build/mcxn947/flash_wolfboot.bin" +MX=(env TARGET=mcxn947 MCXN947_SPSDK_VENV="$T/venv" MCXN947_PROBE_UID=PROBEA MCXN947_WOLFBOOT_DIR="$R/wolfBoot" WT_PROVISION_STATE="$T/st" PATH="$T/venv/bin:$PATH" "$P") +MS="$T/st/mcxn947" +mkdir -p "$R/tests/firmware/mcxn947-baremetal/build" echo mg0 > "$R/tests/firmware/mcxn947-baremetal/build/guest0.bin" echo mg1 > "$R/tests/firmware/mcxn947-baremetal/build/guest1.bin" +# restore runs the hardware runner; here a stub that reports the probe it got. +printf '#!/bin/sh\necho "stub runner $1 probe=${MCXN947_PROBE_UID:-none}"\n' > "$R/tests/target/run_mcxn947_hardware.sh" +chmod +x "$R/tests/target/run_mcxn947_hardware.sh" +mdig() { + local a f + for a in 0x00000000:wolfBoot/wolfboot.bin 0x00040000:build/wolftrust_v1_signed.bin \ + 0x00080000:tests/firmware/mcxn947-baremetal/build/guest0.bin \ + 0x00100000:tests/firmware/mcxn947-baremetal/build/guest1.bin; do + f="$R/${a#*:}"; printf '%s %s\n' "${a%%:*}" "$(wc -c < "$f" | tr -d ' ')"; cat "$f" + done | sha | cut -c1-64 +} +# mrec [probe]: a rehearsal record of this part +mrec() { mkdir -p "$MS"; echo "state=$1 run=r id=11111111222222223333333344444444 image=$(mdig) fused=$2 fence=none probe=${4:-PROBEA} time=$3 cred=none completed=$3" > "$MS/rehearsal-$1"; } # regs [DAUTHSTATUS]: the words the pyocd stub answers # (SYSCON life cycle, debug status, UUID, an intact handoff, the reset vector). regs() { @@ -395,11 +446,9 @@ for m in 0 0x4 junk; do check "MCXN947_GUEST_MASK=$m refused" 2 "MCXN947_GUEST_MASK must be" -- env MCXN947_GUEST_MASK="$m" WT_LOCK_CONFIRM=1 "${MX[@]}" advance 0x07 done check "no guest flash fence is reported" 1 "no guest flash fence" -- "${MX[@]}" verify-wrp -check "lock refused: oem-secure-world" 2 "oem-secure-world (0x07) is not a lock target" -- "${MX[@]}" lock 0x07 check "lock refused: oem-closed by name" 2 "oem-closed (0x0F) is not a lock target" -- "${MX[@]}" lock oem-closed check "lock refused: oem-locked" 2 "is not a lock target" -- "${MX[@]}" lock 0xCF check "lock refused: oem-field-return" 2 "is not a lock target" -- "${MX[@]}" lock 0x1F -check "lock refused with the production opt-in too" 2 "is not a lock target" -- env WT_FIXTURE_BOUND=1 WT_LOCK_CONFIRM=1 WT_PRODUCTION_LOCK=1 "${MX[@]}" lock 0x07 check "advance needs discover" 2 "run 'discover' first" -- env WT_LOCK_CONFIRM=1 "${MX[@]}" advance 0x07 check "status reads the life cycle and the UUID" 0 "device UUID 11111111222222223333333344444444" -- "${MX[@]}" status regs 0x03 0x07 @@ -413,7 +462,42 @@ check "discover stamps the life cycle and UUID" 0 "discovery stamped" -- "${MX[@ check "advance past oem-secure-world needs a regress" 2 "prove 'regress' from oem-secure-world" -- env WT_LOCK_CONFIRM=1 "${MX[@]}" advance 0x0F check "OTP writes refused" 2 "would program OTP" -- "${MX[@]}" burn check "perimeter commands refused" 2 "has no MCXN947 form" -- "${MX[@]}" set-wrp -check "restore has no runner yet" 1 "no MCXN947 hardware runner" -- env WT_LOCK_CONFIRM=1 "${MX[@]}" restore +check "restore runs the hardware runner bound to the probe" 0 "stub runner positive probe=PROBEA" -- env WT_LOCK_CONFIRM=1 "${MX[@]}" restore +# Three probes and no id: every device command refuses loudly, never a +# silent empty read. +printf 'PROBEA\nPROBEB\nPROBEC\n' > "$T/probe" +check "status with three probes and no id refuses" 2 "set MCXN947_PROBE_UID" -- env -u MCXN947_PROBE_UID TARGET=mcxn947 MCXN947_SPSDK_VENV="$T/venv" WT_PROVISION_STATE="$T/st" PATH="$T/venv/bin:$PATH" "$P" status +check "status names the bound probe" 0 "debug probe PROBEA" -- "${MX[@]}" status +check "discover stamps the probe" 0 "probe PROBEA" -- "${MX[@]}" discover +grep -q "PROBE=PROBEA" "$MS/discovery" && { pass=$((pass+1)); echo "ok discovery records the probe"; } || { failn=$((failn+1)); echo "FAIL discovery lacks the probe"; } +: > "$T/pyocd_calls" +# The lock: previews the vendor write from the live pages and refuses it, +# with every confirmation supplied, on the hard-wired port constant. +check "lock needs a rehearsal" 2 "no rehearsal for oem-secure-world" -- "${MX[@]}" lock 0x07 +mrec 0x07 0x03 "$(now)" +out="$("${MX[@]}" lock 0x07 2>&1)"; rc=$? +[ $rc = 2 ] && grep -q "preview only" <<<"$out" && grep -q "efuse-program-once 1 0x7000007 --no-verify" <<<"$out" && grep -q "pfr write: nothing" <<<"$out" && grep -q "hard-wired off" <<<"$out" && grep -q "CMPA/CFPA read and parsed" <<<"$out" && { pass=$((pass+1)); echo "ok lock previews the fuse script and the pages, then stops"; } || { failn=$((failn+1)); echo "FAIL lock preview rc=$rc"; echo "$out" | tail -8; } +[ -s "$MS/lock-0x07/burn-0x07.bls" ] && [ -s "$MS/lock-0x07/cmpa.yaml" ] && [ -s "$MS/lock-0x07/pages.sha256" ] && { pass=$((pass+1)); echo "ok the preview keeps the script, the parsed pages and their digests"; } || { failn=$((failn+1)); echo "FAIL preview artifacts missing"; } +if grep -q . "$T/pyocd_calls" && ! grep -v -- "-u PROBEA" "$T/pyocd_calls" | grep -q .; then pass=$((pass+1)); echo "ok every pyocd call of the preview carried the probe id"; else failn=$((failn+1)); echo "FAIL a pyocd call ran without -u PROBEA: $(grep -v -- '-u PROBEA' "$T/pyocd_calls" | head -2)"; fi +mrec 0x07 0x03 "$(now)" PROBEB +check "a rehearsal on another probe is refused" 2 "no rehearsal for oem-secure-world" -- "${MX[@]}" lock 0x07 +mrec 0x07 0x03 "$(now)" +check "lock refused with the production opt-in but no terminal" 2 "needs an interactive terminal" -- env WT_FIXTURE_BOUND=1 WT_LOCK_CONFIRM=1 WT_PRODUCTION_LOCK=1 "${MX[@]}" lock 0x07 +if [ "$have_expect" = 1 ]; then +MXX="env TARGET=mcxn947 MCXN947_SPSDK_VENV=$T/venv MCXN947_PROBE_UID=PROBEA MCXN947_WOLFBOOT_DIR=$R/wolfBoot WT_PROVISION_STATE=$T/st PATH=$T/venv/bin:$PATH WT_LOCK_CONFIRM=1 WT_PRODUCTION_LOCK=1 $P" +out="$(expect -c "set timeout 300; spawn $MXX lock 0x07; expect \"to continue: \"; send \"I ACCEPT 0x07\r\"; expect eof; catch close; catch wait r; exit [lindex \$r 3]")" +rc=$?; [ $rc = 2 ] && grep -q "hard-wired off in this port (MCXN947_LOCK_WRITE_ENABLED=0)" <<<"$out" && [ -e "$MS/rehearsal-0x07" ] && { pass=$((pass+1)); echo "ok the exact phrase with every confirmation is still refused at the write"; } || { failn=$((failn+1)); echo "FAIL typed acceptance rc=$rc"; echo "$out" | tail -5; } +else + echo "skip typed lock refusal (no expect)" +fi +nowrite +check "verify-pages: unchanged since the preview" 0 "cfpa-pong unchanged since the preview" -- "${MX[@]}" verify-pages +# From a part fused one step further, the closed states are still no lock +# target: the ladder stops them before any preview. +regs 0x07 0x07; mrec 0x0F 0x07 "$(now)" +check "lock refused: oem-closed from oem-secure-world" 2 "oem-closed (0x0F) is not a lock target" -- "${MX[@]}" lock 0x0F +regs 0x0F 0x0F; mrec 0xCF 0x0F "$(now)" +check "lock refused: oem-locked from oem-closed" 2 "oem-locked (0xCF) is not a lock target" -- "${MX[@]}" lock 0xCF nowrite rm -f "$T/mcxn_regs" echo "provisioning gates: $pass passed, $failn failed" From 224b1174c9c3d58e1e0769681be399c071d68be8 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Mon, 26 Oct 2026 17:07:13 +0000 Subject: [PATCH 26/45] Document the MCXN947 lock preview and the vendor flow it was extracted from --- docs/MCXN947-Guide.md | 58 +++++++++++++++++++++++++++++++++++-------- docs/Provisioning.md | 4 +-- 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/docs/MCXN947-Guide.md b/docs/MCXN947-Guide.md index a62578a0..d04fe3b3 100644 --- a/docs/MCXN947-Guide.md +++ b/docs/MCXN947-Guide.md @@ -349,6 +349,17 @@ WT_LOCK_CONFIRM=1 tests/target/provisioning/provisioning_ctrl.sh regress What differs from the MIMXRT700: +- **Every device command is bound to one probe.** The host that owns the + board may carry several CMSIS-DAP probes; `MCXN947_PROBE_UID` names the + board's MCU-Link (the one `pyocd list` shows as `MCU-LINK FRDM-MCXN947`), + and with more than one probe and no id every command refuses instead of + reading nothing. `status` prints the probe, `discover` stamps it, the + rehearsal record carries it, and `lock` accepts only a rehearsal made + through the same probe. +- **`restore` is the hardware runner.** It runs + `tests/target/run_mcxn947_hardware.sh positive` on that probe, so the + images a rehearsal binds are the ones the runner built, flashed and read + back. - **The mock lock is in the handoff record.** The MCXN947 has no life cycle shadow register (`ELS_OTP_LC_STATE` is read-only), so `advance` halts the core at wolfTrust's reset vector, after wolfBoot wrote the handoff record @@ -356,16 +367,43 @@ What differs from the MIMXRT700: with the target state's production value (`0x2000`, `0x3000`, or `0x6000`). Fuses and SYSCON are untouched; `regress` is a hardware reset. The rehearsal records what wolfTrust saw, the guests launched verified, - the images read back, and the device UUID (`0x01100000`). -- **No state can be locked.** The port implements no OTP write: every - `lock`, `burn`, and `provision-da` is refused, and `verify-wrp` reports - that there is no guest flash fence. The real life cycle advance on this - part (the ROM's ISP path) is not validated here. -- **`restore` has no runner yet.** Flash wolfBoot, the signed Secure image, - and both guests by hand, then `discover`. - -`make test-provisioning` runs the MCXN947 gates against stub tools with the -other ports' gates. + the images read back, the device UUID (`0x01100000`), and the probe. +- **`lock` previews the vendor write and refuses it.** The life cycle is an + OTP fuse (SPSDK's fuse map for the part: `LIFE_CYCLE`, index 1, + `LIFECYCLE[7:0]` and its duplicate at bit 24, the same codes the ROM + reflects into SYSCON). `lock oem-secure-world` reads the live CMPA + (`0x01004000`) and the three CFPA pages (`0x01000000` scratch, ping and + pong) over SWD, parses them with `pfr parse`, generates the exact fuse + script with `nxpfuses fuses-script` (`efuse-program-once 1 0x7000007`), + re-exports the CMPA from its parsed configuration with `pfr export` and + diffs it against the live page (the life cycle is not in the page, so the + page is kept), prints all of it as the `will run:` preview, and then + refuses the write on `MCXN947_LOCK_WRITE_ENABLED=0`, a port constant + hard-wired off in this phase. That refusal holds with `WT_LOCK_CONFIRM=1`, + `WT_PRODUCTION_LOCK=1` and the typed acceptance all supplied, which + `make test-provisioning` proves against stubbed vendor tools; + `verify-pages` re-reads the four pages and compares them with the preview's + digests. The closed states (`oem-closed`, `oem-locked`, + `oem-field-return`) are refused earlier: they need the BootROM to + authenticate wolfBoot under a fused root key table hash, which this port + does not build. `burn` and `provision-da` are refused outright. +- **What the vendor flow validated on the board, and what it did not.** + Read-only or reversible SPSDK 3.11 steps ran first and were extracted into + the port: the life cycle and `DAUTHSTATUS` over SWD, the CMPA and CFPA + pages over SWD (the IFR is memory mapped; `pfr read` itself is an ISP + path and was not used), `pfr parse`, `pfr get-template`, `pfr export` of a + closed-state page and its diff, `nxpfuses get-template` and + `nxpfuses fuses-script` for the next and the closed state, + `nxpdebugmbox tool list-probes`, `mem-tool test-connection` and + `tool get-uuid`. `nxpdebugmbox cmd get-dac` is not offered for this part + by SPSDK 3.11. Nothing that writes ran: no `pfr write`, no `blhost`, no + `nxpdebugmbox ispmode`, `program-lifecycle` or `erase`, no fuse, CMPA, + CFPA, IFR or debug-authentication change. + +`make test-provisioning` runs the MCXN947 gates against stub `pyocd`, +`pfr`, `nxpfuses`, `nxpdebugmbox` and `blhost` with the other ports' gates; +`tests/host/mcxn947_lifecycle` checks the life cycle codes against SPSDK's +fuse map and that every ladder step only sets fuse bits. ## Open items diff --git a/docs/Provisioning.md b/docs/Provisioning.md index 9a35918f..6ea931da 100644 --- a/docs/Provisioning.md +++ b/docs/Provisioning.md @@ -19,7 +19,7 @@ guide covers its states, quirks, and a walkthrough with real output: - [STM32H5 Guide: Provisioning and product state](STM32H5-Guide.md#provisioning-and-product-state) - [MIMXRT700 Guide: Provisioning and life cycle](MIMXRT700-Guide.md#provisioning-and-life-cycle) - [MCXN947 Guide: Provisioning and life cycle](MCXN947-Guide.md#provisioning-and-life-cycle) - (rehearsal only: this port locks nothing) + (rehearsal and lock preview only: this port writes no life cycle) > **Production locks are permanent.** `lock` is for a production station and a > part you intend to ship, never a development board. Every step before it is @@ -202,7 +202,7 @@ with it can never be updated; a leaked one means they trust whoever holds it. | `WT_DA_OBK`, `WT_DA_KEY`, `WT_DA_CERT`, `WT_DA_PWD` | STM32H5 Debug Authentication inputs; a production lock requires all four, none of them ST's sample | | `STM32_CLI`, `H5_SERIAL` | STM32H5: STM32CubeProgrammer CLI and the board UART | | `RT700_ISP`, `RT700_SPSDK_VENV`, `RT700_GUEST_MASK` | MIMXRT700: blhost ISP connection, SPSDK environment, guests a rehearsal must launch | -| `MCXN947_SPSDK_VENV`, `MCXN947_GUEST_MASK`, `MCXN947_TARGET` | MCXN947: the pyOCD environment, guests a rehearsal must launch, and the pyOCD target name | +| `MCXN947_PROBE_UID`, `MCXN947_SPSDK_VENV`, `MCXN947_GUEST_MASK`, `MCXN947_TARGET`, `MCXN947_WOLFBOOT_DIR`, `MCXN947_ISP` | MCXN947: the board's MCU-Link unique id (required when the host has more than one probe), the SPSDK environment, guests a rehearsal must launch, the pyOCD target name, the wolfBoot tree the runner flashed, and the blhost ISP connection the lock preview names | ## Adding a port From 4b802e2d65234137ddd4014a370f98d5016d28ac Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Mon, 26 Oct 2026 20:57:46 +0000 Subject: [PATCH 27/45] Reserve both write-once trailer sectors in the MCXN947 FWU backend --- port/mcxn947/hsm_flash.c | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/port/mcxn947/hsm_flash.c b/port/mcxn947/hsm_flash.c index c54aabf2..a9ab77a0 100644 --- a/port/mcxn947/hsm_flash.c +++ b/port/mcxn947/hsm_flash.c @@ -382,6 +382,9 @@ int wt_hsm_flash_format(void) static uint32_t g_fwu_erased_end; static uint32_t g_fwu_written_end; +/* Write-once wolfBoot keeps a trailer copy in each of the last two sectors. */ +#define WT_FWU_TRAILER_SIZE (2u * WT_FLASH_SECTOR_SIZE) + static int wt_fwu_ensure_erased(uint32_t offset, uint32_t size) { uint32_t sector_size = g_fwu_flash_cfg.sector_size; @@ -414,11 +417,11 @@ static int wt_fwu_backend_write(void *ctx, uint32_t offset, const uint8_t *data, uint32_t size) { const uint8_t *mapped = (const uint8_t *)(g_fwu_flash_cfg.base + offset); - uint32_t reserved = g_fwu_flash_cfg.size - g_fwu_flash_cfg.sector_size; + uint32_t reserved = g_fwu_flash_cfg.size - WT_FWU_TRAILER_SIZE; (void)ctx; - /* The trailer sector holds wolfBoot's swap trigger and is owned by - * arm/disarm alone; staging data must never pre-program it. */ + /* The trailer sectors hold wolfBoot's swap trigger and are owned by + * arm/disarm alone; staging data must never pre-program them. */ if (offset >= reserved || size > reserved - offset || offset != g_fwu_written_end) { return -1; @@ -450,8 +453,8 @@ static int wt_fwu_backend_arm(void *ctx, uint32_t image_size, (void)image_size; (void)version; /* The wolfBoot trigger lives in the topmost program unit of the partition - * (state + magic); the block below it stays erased. The trailer sector - * never holds staged data, so it is erased outright. */ + * (state + magic); the block below it stays erased. The trailer sectors + * never hold staged data, so they are erased outright. */ if (wt_fwu_wolfboot_arm_trailer(block, (uint32_t)sizeof(block)) != 0) { return -1; } @@ -470,11 +473,11 @@ static int wt_fwu_backend_arm(void *ctx, uint32_t image_size, static int wt_fwu_backend_disarm(void *ctx) { - uint32_t trailer = g_fwu_flash_cfg.size - g_fwu_flash_cfg.sector_size; + uint32_t trailer = g_fwu_flash_cfg.size - WT_FWU_TRAILER_SIZE; (void)ctx; if (wt_hsm_flash_erase(&g_fwu_flash_ctx, trailer, - g_fwu_flash_cfg.sector_size) != WH_ERROR_OK) { + WT_FWU_TRAILER_SIZE) != WH_ERROR_OK) { return -1; } return 0; @@ -543,8 +546,8 @@ const wt_fwu_backend_t wt_fwu_flash_backend = { .write = wt_fwu_backend_write, .arm = wt_fwu_backend_arm, .disarm = wt_fwu_backend_disarm, - /* The trailer sector is arm/disarm-owned, never staging capacity. */ - .capacity = WT_FWU_UPDATE_FLASH_SIZE - WT_FLASH_SECTOR_SIZE, + /* The trailer sectors are arm/disarm-owned, never staging capacity. */ + .capacity = WT_FWU_UPDATE_FLASH_SIZE - WT_FWU_TRAILER_SIZE, .align = WT_FLASH_PHRASE_SIZE, .verify = wt_fwu_backend_verify, }; From 743335b34d76bf443a5c8642fb5c5ace49f906b7 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Mon, 26 Oct 2026 20:57:46 +0000 Subject: [PATCH 28/45] Accept a padded final phrase when verifying an MCXN947 FWU candidate --- port/mcxn947/hsm_flash.c | 5 ++++- tests/firmware/psa-guest/Makefile | 6 ++++++ tests/firmware/psa-guest/main.c | 11 +++++++---- tests/target/run_mcxn947_m33mu.sh | 5 +++-- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/port/mcxn947/hsm_flash.c b/port/mcxn947/hsm_flash.c index a9ab77a0..e5627f6d 100644 --- a/port/mcxn947/hsm_flash.c +++ b/port/mcxn947/hsm_flash.c @@ -501,9 +501,12 @@ static int wt_fwu_backend_verify(void *ctx, uint32_t staged_size, int found = 0; (void)ctx; + /* FINISH passes the logical size; the programmed extent is it rounded up + * to a phrase. */ if (header_version == NULL || staged_size < WT_FWU_IMAGE_HEADER_SIZE || - staged_size != g_fwu_written_end) { + staged_size > g_fwu_written_end || + g_fwu_written_end - staged_size >= WT_FLASH_PHRASE_SIZE) { return -1; } memcpy(&magic, hdr, sizeof(magic)); diff --git a/tests/firmware/psa-guest/Makefile b/tests/firmware/psa-guest/Makefile index abe07cb7..79b05235 100644 --- a/tests/firmware/psa-guest/Makefile +++ b/tests/firmware/psa-guest/Makefile @@ -43,6 +43,9 @@ WT_FWU_PROBE ?= 0 WT_FWU_PROBE_STREAM_BYTES ?= # The backend under test refuses a write that skips ahead of the staged extent. WT_FWU_PROBE_SEQUENTIAL ?= 0 +# Bytes of the FWU probe's final write; a size off the backend alignment proves +# FINISH accepts a padded last block. +WT_FWU_PROBE_TAIL_BYTES ?= # The attestation verify pins the token measurement to wolfBoot's measurement # of the signed Secure image and the lifecycle the Secure side reports. WT_EXPECTED_MEASUREMENT_HEX ?= @@ -286,6 +289,9 @@ endif ifeq ($(WT_FWU_PROBE_SEQUENTIAL),1) PROBE_CFLAGS += -DWT_FWU_PROBE_SEQUENTIAL endif +ifneq ($(WT_FWU_PROBE_TAIL_BYTES),) +PROBE_CFLAGS += -DWT_FWU_PROBE_TAIL_BYTES=$(WT_FWU_PROBE_TAIL_BYTES) +endif # guest0 carries the probes; guest1 is the peer that must keep running. GUEST0_CFLAGS := -DGUEST_NAME=\"guest0\" -DWT_WOLFHSM_CLIENT_ID=1u \ diff --git a/tests/firmware/psa-guest/main.c b/tests/firmware/psa-guest/main.c index af9d1519..311c6a3c 100644 --- a/tests/firmware/psa-guest/main.c +++ b/tests/firmware/psa-guest/main.c @@ -848,6 +848,9 @@ static void exercise_hsm_attack_probe(void) * wolfBoot update partition and verifies each block by read-back, then the * PSA FWU 1.0 reject/clean tail restores READY. */ #define GUEST_FWU_OP_QUERY 1u +#if !defined(WT_FWU_PROBE_TAIL_BYTES) +#define WT_FWU_PROBE_TAIL_BYTES 32u +#endif #define GUEST_FWU_OP_START 2u #define GUEST_FWU_OP_WRITE 3u #define GUEST_FWU_OP_FINISH 4u @@ -986,11 +989,11 @@ static void exercise_fwu(void) memset(&req, 0, sizeof(req)); req.offset = tail_off; - req.size = 32u; + req.size = WT_FWU_PROBE_TAIL_BYTES; memcpy(writebuf, &req, sizeof(req)); - memset(writebuf + 16, 0x22, 32u); - st = guest_fwu_call(handle, GUEST_FWU_OP_WRITE, writebuf, 16u + 32u, - NULL, 0u); + memset(writebuf + 16, 0x22, WT_FWU_PROBE_TAIL_BYTES); + st = guest_fwu_call(handle, GUEST_FWU_OP_WRITE, writebuf, + 16u + WT_FWU_PROBE_TAIL_BYTES, NULL, 0u); if (st != 0) { guest_line_i32("wolfTrust FWU write#2 failed st=", st); ok = 0; diff --git a/tests/target/run_mcxn947_m33mu.sh b/tests/target/run_mcxn947_m33mu.sh index a1b1680e..767816c5 100755 --- a/tests/target/run_mcxn947_m33mu.sh +++ b/tests/target/run_mcxn947_m33mu.sh @@ -182,8 +182,9 @@ case "$scenario" in restart) guest_flags="WT_GUEST_FAULT_PROBE=1" ;; attestneg) guest_flags="WT_ATTEST_NEG_PROBE=1" ;; hsmattackneg) guest_flags="WT_HSM_ATTACK_PROBE=1" ;; - # 0x21000 bytes of body reach sector 16 of the 32-sector update partition. - fwustage) guest_flags="WT_FWU_PROBE=1 WT_FWU_PROBE_STREAM_BYTES=0x21000u WT_FWU_PROBE_SEQUENTIAL=1" ;; + # 0x21000 bytes of body reach sector 16 of the 32-sector update partition; + # the 27-byte tail ends off a phrase, so FINISH sees a padded last block. + fwustage) guest_flags="WT_FWU_PROBE=1 WT_FWU_PROBE_STREAM_BYTES=0x21000u WT_FWU_PROBE_SEQUENTIAL=1 WT_FWU_PROBE_TAIL_BYTES=27u" ;; confboot) guest_flags="WT_RUN_CONFORMANCE=1 WT_M33MU_EXPECT_BKPT=1" ;; devstorage) guest_flags="WT_RUN_CONFORMANCE=1 WT_CONF_SUITE=storage WT_M33MU_EXPECT_BKPT=1" ;; devcrypto|vaultrecover|vaultrecoversec) From 2a659c6187e83131fcc6184a6dcc3cbc5417db62 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Mon, 26 Oct 2026 20:57:46 +0000 Subject: [PATCH 29/45] Name the generated secure owner map after its target --- mk/arch-armv8m.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mk/arch-armv8m.mk b/mk/arch-armv8m.mk index 22a9e243..bf8c62b9 100644 --- a/mk/arch-armv8m.mk +++ b/mk/arch-armv8m.mk @@ -104,7 +104,7 @@ ARCH_LDFLAGS = -Wl,--cmse-implib -Wl,--out-implib=$(SECURE_CMSE_IMPLIB) \ # The owner map the post-link layout check reads: the shared map plus the # port's own objects (port//secure_owners.txt), so a port adds its # owners without editing the shared file. -WT_SECURE_OWNERS := $(BUILD_DIR)/secure_owners.txt +WT_SECURE_OWNERS := $(BUILD_DIR)/secure_owners-$(TARGET).txt $(WT_SECURE_OWNERS): $(ROOT)/tools/secure_owners.txt \ $(wildcard $(PORT_DIR)/secure_owners.txt) | $(BUILD_DIR) cat $^ > $@ From c65cf9ebae288be8ae784f56a61291d0a4b4efe5 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Mon, 26 Oct 2026 20:57:46 +0000 Subject: [PATCH 30/45] Stop MCXN947 board programming when pyOCD exits nonzero --- tests/target/run_mcxn947_hardware.sh | 10 +++++++--- tests/target/test_mcxn947_suite_evidence.sh | 22 +++++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/tests/target/run_mcxn947_hardware.sh b/tests/target/run_mcxn947_hardware.sh index 1fc8f415..80fc8c8d 100755 --- a/tests/target/run_mcxn947_hardware.sh +++ b/tests/target/run_mcxn947_hardware.sh @@ -160,12 +160,16 @@ clear_mailboxes() { -c "write32 $verified 0" -c "write32 $refused 0") } -# The commander exits 0 whatever a command did, so its output is the verdict. +# The commander exits 0 whatever a command did, so its output is checked too. program_board() { - local out + local out rc=0 [ "${#board_cmds[@]}" -gt 0 ] || fail "nothing to program" out="$(timeout 900 pyocd cmd -u "$probe_uid" -t "$target" -O resume_on_disconnect=false \ - "${park_cmds[@]}" "${board_cmds[@]}" 2>&1)" || true + "${park_cmds[@]}" "${board_cmds[@]}" 2>&1)" || rc=$? + if [ "$rc" -ne 0 ]; then + printf '%s\n' "$out" | tail -6 + fail "programming the board failed (pyocd exit $rc)" + fi if printf '%s\n' "$out" | grep -qiE 'error|exception|not halted|failure|not within|not in flash'; then printf '%s\n' "$out" | grep -iE 'error|exception|not halted|failure|not within|not in flash' | tail -6 fail "programming the board failed" diff --git a/tests/target/test_mcxn947_suite_evidence.sh b/tests/target/test_mcxn947_suite_evidence.sh index 97dcd39a..494c6ef7 100755 --- a/tests/target/test_mcxn947_suite_evidence.sh +++ b/tests/target/test_mcxn947_suite_evidence.sh @@ -66,5 +66,27 @@ wait "$holder" "$suite" >/dev/null 2>&1 check "$([ -s "$tmp/logs/beta.log" ]; echo $?)" "the batch runs once the lock is released" +# program_board must stop before the core is released when pyOCD exits nonzero +# without saying why (a timeout) or exits 0 reporting a failed command. +sed -n '/^program_board() {/,/^}/p' "$runner" > "$tmp/program_board.sh" +program_case() { + bash -c ' +fail() { echo "FAIL: $*"; exit 1; } +timeout() { shift; "$@"; } +pyocd() { case "$PB_MODE" in + silent) return 124 ;; error) echo "Error: transfer failed"; return 0 ;; + *) echo "loaded"; return 0 ;; esac; } +probe_uid=X; target=mcxn947vdf +park_cmds=(-c "reset halt"); board_cmds=(-c "load a.bin 0x0") +. "$1" +program_board && echo RELEASED' _ "$tmp/program_board.sh" 2>&1 +} +check "$(! PB_MODE=silent program_case | grep -q RELEASED; echo $?)" \ + "a silent pyOCD timeout stops programming" +check "$(! PB_MODE=error program_case | grep -q RELEASED; echo $?)" \ + "a command error reported with exit 0 stops programming" +check "$(PB_MODE=ok program_case | grep -q RELEASED; echo $?)" \ + "a clean session releases the core" + echo "mcxn947 suite evidence: $fails failures" [ "$fails" -eq 0 ] From 7cb8588e68dff8f88c72c8cc8a4a470a845aa0cc Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Mon, 26 Oct 2026 21:17:57 +0000 Subject: [PATCH 31/45] Keep the MCXN947 ELS RNG page off the stack and stop after a timeout --- port/mcxn947/mcxn947_regs.h | 2 + port/mcxn947/rng_entropy.c | 16 +++- tests/host/Makefile | 2 +- tests/host/mcxn947_rng/Makefile | 70 ++++++++++++++ tests/host/mcxn947_rng/main.c | 148 ++++++++++++++++++++++++++++++ tests/host/mcxn947_rng/mock_els.h | 34 +++++++ 6 files changed, 266 insertions(+), 6 deletions(-) create mode 100644 tests/host/mcxn947_rng/Makefile create mode 100644 tests/host/mcxn947_rng/main.c create mode 100644 tests/host/mcxn947_rng/mock_els.h diff --git a/port/mcxn947/mcxn947_regs.h b/port/mcxn947/mcxn947_regs.h index c3676759..f245d8e7 100644 --- a/port/mcxn947/mcxn947_regs.h +++ b/port/mcxn947/mcxn947_regs.h @@ -43,7 +43,9 @@ #define WT_ROM_SIZE 0x00040000u #define WT_ROM_API_TREE 0x1303FC00u +#ifndef WT_REG32 #define WT_REG32(address) (*(volatile uint32_t*)(address)) +#endif /* EdgeLock (ELS, S50) command interface; register offsets per the MCUXpresso * S50 peripheral header. */ diff --git a/port/mcxn947/rng_entropy.c b/port/mcxn947/rng_entropy.c index e684623a..91e304e6 100644 --- a/port/mcxn947/rng_entropy.c +++ b/port/mcxn947/rng_entropy.c @@ -37,6 +37,11 @@ int wolftrust_rng_generate_block_direct(unsigned char *output, #define WT_ELS_PAGE_WORDS 16u static bool s_rng_ready; +/* A request that outlives its deadline may still complete by DMA, so the page + * is static rather than stack and the engine is never asked again. */ +static bool s_rng_failed; +static uint32_t s_els_page[WT_ELS_PAGE_WORDS] + __attribute__((aligned(WT_ELS_RND_ALIGN))); /* Read over the debug port by the hardware runner. */ volatile uint32_t g_wt_els_errors __attribute__((used)); volatile uint32_t g_wt_els_last_status __attribute__((used)); @@ -82,6 +87,7 @@ static int wt_els_rnd_page(uint32_t *page) WT_ELS_CTRL_CMD(WT_ELS_CMD_RND_REQ); ret = wt_els_wait_idle(); if (ret != 0) { + s_rng_failed = true; WT_ELS_ERR_STATUS_CLR = WT_ELS_ERR_STATUS_CLR_ALL; } } @@ -90,8 +96,8 @@ static int wt_els_rnd_page(uint32_t *page) static int wt_els_fill(unsigned char *output, unsigned int sz) { - uint32_t page[WT_ELS_PAGE_WORDS] __attribute__((aligned(WT_ELS_RND_ALIGN))); - volatile uint32_t* wipe = page; + uint32_t *page = s_els_page; + volatile uint32_t* wipe = s_els_page; unsigned int done = 0u; unsigned int chunk; unsigned int i; @@ -101,8 +107,8 @@ static int wt_els_fill(unsigned char *output, unsigned int sz) ret = wt_els_rnd_page(page); if (ret == 0) { chunk = sz - done; - if (chunk > (unsigned int)sizeof(page)) { - chunk = (unsigned int)sizeof(page); + if (chunk > (unsigned int)sizeof(s_els_page)) { + chunk = (unsigned int)sizeof(s_els_page); } (void)memcpy(output + done, page, chunk); done += chunk; @@ -116,7 +122,7 @@ static int wt_els_fill(unsigned char *output, unsigned int sz) int wolftrust_rng_generate_block_direct(unsigned char *output, unsigned int sz) { - if (output == NULL && sz != 0u) { + if ((output == NULL && sz != 0u) || s_rng_failed) { return -1; } if (!s_rng_ready) { diff --git a/tests/host/Makefile b/tests/host/Makefile index 49d9356f..9596c8b7 100644 --- a/tests/host/Makefile +++ b/tests/host/Makefile @@ -33,7 +33,7 @@ endif UNIT_SUITES := domain manifest lifecycle monitor guest_verify rollback sp_recovery \ fabric_windows periph irq_claim rt700_trng rt700_xspi rt700_lifecycle \ - mcxn947_flash mcxn947_lifecycle \ + mcxn947_flash mcxn947_lifecycle mcxn947_rng \ ipc ffm spm \ vnet wolfhsm_loopback wolfhsm_relay native_wire native_wire_client \ keystore_isolation \ diff --git a/tests/host/mcxn947_rng/Makefile b/tests/host/mcxn947_rng/Makefile new file mode 100644 index 00000000..716832de --- /dev/null +++ b/tests/host/mcxn947_rng/Makefile @@ -0,0 +1,70 @@ +# 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 := \ + -I$(ROOT)/include \ + -I$(ROOT)/port/mcxn947 \ + -include mock_els.h \ + -std=c99 -O0 -g -Wall -Wextra -pedantic +CFLAGS += $(EXTRA_CFLAGS) + +ALL_OBJS := $(BUILD_DIR)/wt_rng_entropy.o $(BUILD_DIR)/main.o +TEST_BIN := $(BUILD_DIR)/test_mcxn947_rng + +.PHONY: all run compilers sanitize valgrind clean + +all: $(TEST_BIN) + +$(BUILD_DIR): + mkdir -p $@ + +$(BUILD_DIR)/wt_%.o: $(ROOT)/port/mcxn947/%.c mock_els.h | $(BUILD_DIR) + $(CC) $(CFLAGS) -c -o $@ $< + +$(BUILD_DIR)/%.o: %.c mock_els.h | $(BUILD_DIR) + $(CC) $(CFLAGS) -c -o $@ $< + +$(TEST_BIN): $(ALL_OBJS) + $(CC) $(CFLAGS) $(EXTRA_LDFLAGS) -o $@ $(ALL_OBJS) + +run: $(TEST_BIN) + $(TEST_BIN) + +compilers: + $(MAKE) clean run CC=gcc BUILD_DIR=$(BUILD_DIR)/gcc + $(MAKE) clean run CC=clang BUILD_DIR=$(BUILD_DIR)/clang + +sanitize: + $(MAKE) clean run CC=clang \ + BUILD_DIR=$(BUILD_DIR)/sanitize \ + EXTRA_CFLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" \ + EXTRA_LDFLAGS="-fsanitize=address,undefined" + +valgrind: clean $(TEST_BIN) + valgrind --error-exitcode=1 --leak-check=full --show-leak-kinds=all \ + $(TEST_BIN) + +clean: + rm -rf $(BUILD_DIR) diff --git a/tests/host/mcxn947_rng/main.c b/tests/host/mcxn947_rng/main.c new file mode 100644 index 00000000..5a7424e1 --- /dev/null +++ b/tests/host/mcxn947_rng/main.c @@ -0,0 +1,148 @@ +/* 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 . + */ + + +/* Host suite for the MCXN947 ELS entropy driver over a model of the RND_REQ + * command: a request that misses its deadline must target memory outside the + * stack and must leave the source refusing every later call. The register + * holds 32 bits, so only the low half of a host address is compared. */ + +#include +#include +#include + +#include "mcxn947_regs.h" +#include "wolftrust/spm_transport.h" + +#define WT_ELS_PAGE_BYTES 64u + +int wolftrust_rng_generate_block(unsigned char *output, unsigned int sz); + +static int checks; +static int failures; + +#define EXPECT(cond, what) \ + do { \ + checks++; \ + if (!(cond)) { \ + (void)fprintf(stderr, "line %d: %s\n", __LINE__, (what)); \ + failures++; \ + } \ + } while (0) + +static volatile uint32_t g_status; +static volatile uint32_t g_ctrl; +static volatile uint32_t g_scratch; +static uint32_t g_dma_addr; +static uint32_t g_dma_len; +static uint32_t g_requests; +static uint32_t g_last_dma_addr; +static int g_hang; +static uint32_t g_ctrl_seen; + +/* A START seen on the next status read issues the request: it completes at + * once, or with g_hang stays BUSY past the driver's deadline. */ +static void wt_mock_step(void) +{ + if (g_ctrl != g_ctrl_seen) { + if ((g_ctrl & WT_ELS_CTRL_START) != 0u) { + /* START self-clears, so an identical later write is a new one. */ + g_ctrl &= ~WT_ELS_CTRL_START; + g_requests++; + g_last_dma_addr = g_dma_addr; + g_status = g_hang ? WT_ELS_STATUS_BUSY : 0u; + } + g_ctrl_seen = g_ctrl; + } +} + +volatile uint32_t* wt_mock_reg(uintptr_t address) +{ + switch (address) { + case WT_ELS_BASE_S + 0x00u: + wt_mock_step(); + return &g_status; + case WT_ELS_BASE_S + 0x04u: + return &g_ctrl; + case WT_ELS_BASE_S + 0x38u: + return (volatile uint32_t *)&g_dma_addr; + case WT_ELS_BASE_S + 0x3Cu: + return (volatile uint32_t *)&g_dma_len; + default: + return &g_scratch; + } +} + +int wt_spm_sp_call(struct wt_spm_call* call) +{ + (void)call; + return -1; +} + +static int on_stack(uint32_t addr, const void *marker) +{ + uint32_t m = (uint32_t)(uintptr_t)marker; + uint32_t d = (addr > m) ? addr - m : m - addr; + + return d < 0x10000u; +} + +static void test_completed_request(void) +{ + unsigned char out[40]; + uint8_t marker = 0u; + + g_hang = 0; + memset(out, 0, sizeof(out)); + EXPECT(wolftrust_rng_generate_block(out, sizeof(out)) == 0, + "a completed RND_REQ serves its words"); + EXPECT(g_requests == 1u, "40 bytes take one page"); + EXPECT(g_dma_len == WT_ELS_PAGE_BYTES, "a request covers a whole page"); + EXPECT(!on_stack(g_last_dma_addr, &marker), + "the DMA page is not on the caller's stack"); +} + +static void test_late_completion(void) +{ + unsigned char out[16]; + uint8_t marker = 0u; + uint32_t before; + + g_hang = 1; + EXPECT(wolftrust_rng_generate_block(out, sizeof(out)) != 0, + "a request past its deadline fails"); + EXPECT(!on_stack(g_last_dma_addr, &marker), + "the timed-out request's DMA target is not on the stack"); + /* The request completes after the driver gave up. */ + g_status = 0u; + before = g_requests; + g_hang = 0; + EXPECT(wolftrust_rng_generate_block(out, sizeof(out)) != 0, + "the source refuses every call after a timeout"); + EXPECT(g_requests == before, "no new request is issued after a timeout"); +} + +int main(void) +{ + test_completed_request(); + test_late_completion(); + (void)printf("mcxn947 rng: %d checks, %d failures\n", checks, failures); + return failures == 0 ? 0 : 1; +} diff --git a/tests/host/mcxn947_rng/mock_els.h b/tests/host/mcxn947_rng/mock_els.h new file mode 100644 index 00000000..08db7d62 --- /dev/null +++ b/tests/host/mcxn947_rng/mock_els.h @@ -0,0 +1,34 @@ +/* mock_els.h + * + * 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 . + */ + + +/* Every MCXN947 register access in the driver under test goes through the + * model in main.c, which plays the ELS command interface. */ + +#ifndef WT_TEST_MOCK_ELS_H +#define WT_TEST_MOCK_ELS_H + +#include + +volatile uint32_t* wt_mock_reg(uintptr_t address); + +#define WT_REG32(address) (*wt_mock_reg((uintptr_t)(address))) + +#endif /* WT_TEST_MOCK_ELS_H */ From 3890e1d4b0f1220c054be5d4024a397ea6536ba1 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Mon, 26 Oct 2026 21:17:57 +0000 Subject: [PATCH 32/45] Refuse a failed MCXN947 PFR page read instead of a stale file --- tests/target/provisioning/provisioning_ctrl_mcxn947.sh | 9 +++++++-- tests/target/provisioning/test_provisioning_gates.sh | 9 ++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/target/provisioning/provisioning_ctrl_mcxn947.sh b/tests/target/provisioning/provisioning_ctrl_mcxn947.sh index 0629372a..673d3e7c 100644 --- a/tests/target/provisioning/provisioning_ctrl_mcxn947.sh +++ b/tests/target/provisioning/provisioning_ctrl_mcxn947.sh @@ -144,9 +144,14 @@ read_words() { pyocd_probe 60 cmd -t cortex_m "${cmds[@]}" 2>&1 | awk '/^[0-9a-f]+:/ { print $2 }' } # One PFR page (512 bytes) over SWD into a file; the IFR is memory mapped. +# The commander exits 0 after a failed command, so its output is checked and +# a stale file from an earlier read never stands in for this one. read_page() { - pyocd_probe 60 cmd -t cortex_m -c "savemem $1 $PFR_PAGE_SIZE $2" >/dev/null 2>&1 && - [ "$(wc -c < "$2" | tr -d ' ')" = "$PFR_PAGE_SIZE" ] + local out + rm -f "$2" + out="$(pyocd_probe 60 cmd -t cortex_m -c "savemem $1 $PFR_PAGE_SIZE $2" 2>&1)" || return 1 + ! printf '%s\n' "$out" | grep -qiE 'error|exception|fault|failure' && + [ -f "$2" ] && [ "$(wc -c < "$2" | tr -d ' ')" = "$PFR_PAGE_SIZE" ] } page_digest() { sha256 < "$1" | cut -c1-64; } psa_name() { diff --git a/tests/target/provisioning/test_provisioning_gates.sh b/tests/target/provisioning/test_provisioning_gates.sh index 1b4455ee..63213c03 100755 --- a/tests/target/provisioning/test_provisioning_gates.sh +++ b/tests/target/provisioning/test_provisioning_gates.sh @@ -103,7 +103,9 @@ if [ "\$1" = cmd ]; then "read32 "*) k=\$(printf '%08x' "\${a#read32 }" 2>/dev/null || echo 0) v=\$(awk -v k="\$k" '\$1 == k { print \$2 }' "$T/mcxn_regs") echo "\$k: \${v:-00000000}" ;; - "savemem "*) set -- \$a; head -c "\$3" /dev/zero | tr '\\000' '\\377' > "\$4" ;; + "savemem "*) set -- \$a + if [ -e "$T/savemem_err" ]; then echo "Error: memory transfer fault"; continue; fi + head -c "\$3" /dev/zero | tr '\\000' '\\377' > "\$4" ;; esac done exit 0 @@ -492,6 +494,11 @@ else fi nowrite check "verify-pages: unchanged since the preview" 0 "cfpa-pong unchanged since the preview" -- "${MX[@]}" verify-pages +# A savemem that fails while the commander exits 0 must not let the previous +# readback stand in for the page. +touch "$T/savemem_err" +check "verify-pages: a failed read is never the earlier readback" 1 "cannot read cmpa over SWD" -- "${MX[@]}" verify-pages +rm -f "$T/savemem_err" # From a part fused one step further, the closed states are still no lock # target: the ladder stops them before any preview. regs 0x07 0x07; mrec 0x0F 0x07 "$(now)" From 1adff2c2112a803e5317b5c0cae7ebce0e53234a Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Mon, 26 Oct 2026 21:49:53 +0000 Subject: [PATCH 33/45] Match MCXN947 pyOCD errors without a pipe that pipefail can break --- tests/target/provisioning/provisioning_ctrl_mcxn947.sh | 2 +- tests/target/provisioning/test_provisioning_gates.sh | 7 +++++-- tests/target/run_mcxn947_hardware.sh | 4 +++- tests/target/test_mcxn947_suite_evidence.sh | 4 ++++ 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/tests/target/provisioning/provisioning_ctrl_mcxn947.sh b/tests/target/provisioning/provisioning_ctrl_mcxn947.sh index 673d3e7c..b036e5fc 100644 --- a/tests/target/provisioning/provisioning_ctrl_mcxn947.sh +++ b/tests/target/provisioning/provisioning_ctrl_mcxn947.sh @@ -150,7 +150,7 @@ read_page() { local out rm -f "$2" out="$(pyocd_probe 60 cmd -t cortex_m -c "savemem $1 $PFR_PAGE_SIZE $2" 2>&1)" || return 1 - ! printf '%s\n' "$out" | grep -qiE 'error|exception|fault|failure' && + ! grep -qiE 'error|exception|fault|failure' <<<"$out" && [ -f "$2" ] && [ "$(wc -c < "$2" | tr -d ' ')" = "$PFR_PAGE_SIZE" ] } page_digest() { sha256 < "$1" | cut -c1-64; } diff --git a/tests/target/provisioning/test_provisioning_gates.sh b/tests/target/provisioning/test_provisioning_gates.sh index 63213c03..88921a9f 100755 --- a/tests/target/provisioning/test_provisioning_gates.sh +++ b/tests/target/provisioning/test_provisioning_gates.sh @@ -104,8 +104,11 @@ if [ "\$1" = cmd ]; then v=\$(awk -v k="\$k" '\$1 == k { print \$2 }' "$T/mcxn_regs") echo "\$k: \${v:-00000000}" ;; "savemem "*) set -- \$a - if [ -e "$T/savemem_err" ]; then echo "Error: memory transfer fault"; continue; fi - head -c "\$3" /dev/zero | tr '\\000' '\\377' > "\$4" ;; + head -c "\$3" /dev/zero | tr '\\000' '\\377' > "\$4" + # A failed read: the file is written but the commander reports an + # error, ahead of a long log, and still exits 0. + if [ -e "$T/savemem_err" ]; then echo "Error: memory transfer fault" + printf '%02000000d\\n' 0; fi ;; esac done exit 0 diff --git a/tests/target/run_mcxn947_hardware.sh b/tests/target/run_mcxn947_hardware.sh index 80fc8c8d..3205f649 100755 --- a/tests/target/run_mcxn947_hardware.sh +++ b/tests/target/run_mcxn947_hardware.sh @@ -170,7 +170,9 @@ program_board() { printf '%s\n' "$out" | tail -6 fail "programming the board failed (pyocd exit $rc)" fi - if printf '%s\n' "$out" | grep -qiE 'error|exception|not halted|failure|not within|not in flash'; then + # A here-string, not a pipe: under pipefail an early grep -q exit can + # SIGPIPE the writer and hide a match in a long log. + if grep -qiE 'error|exception|not halted|failure|not within|not in flash' <<<"$out"; then printf '%s\n' "$out" | grep -iE 'error|exception|not halted|failure|not within|not in flash' | tail -6 fail "programming the board failed" fi diff --git a/tests/target/test_mcxn947_suite_evidence.sh b/tests/target/test_mcxn947_suite_evidence.sh index 494c6ef7..8803b451 100755 --- a/tests/target/test_mcxn947_suite_evidence.sh +++ b/tests/target/test_mcxn947_suite_evidence.sh @@ -71,10 +71,12 @@ check "$([ -s "$tmp/logs/beta.log" ]; echo $?)" "the batch runs once the lock is sed -n '/^program_board() {/,/^}/p' "$runner" > "$tmp/program_board.sh" program_case() { bash -c ' +set -euo pipefail fail() { echo "FAIL: $*"; exit 1; } timeout() { shift; "$@"; } pyocd() { case "$PB_MODE" in silent) return 124 ;; error) echo "Error: transfer failed"; return 0 ;; + longerror) echo "Error: transfer failed"; printf "%02000000d\n" 0; return 0 ;; *) echo "loaded"; return 0 ;; esac; } probe_uid=X; target=mcxn947vdf park_cmds=(-c "reset halt"); board_cmds=(-c "load a.bin 0x0") @@ -85,6 +87,8 @@ check "$(! PB_MODE=silent program_case | grep -q RELEASED; echo $?)" \ "a silent pyOCD timeout stops programming" check "$(! PB_MODE=error program_case | grep -q RELEASED; echo $?)" \ "a command error reported with exit 0 stops programming" +check "$(! PB_MODE=longerror program_case | grep -q RELEASED; echo $?)" \ + "a command error ahead of a long log stops programming" check "$(PB_MODE=ok program_case | grep -q RELEASED; echo $?)" \ "a clean session releases the core" From ee61f033c400d1158aa6e87d34af1a0f62a13113 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Mon, 26 Oct 2026 21:49:53 +0000 Subject: [PATCH 34/45] Build the MCXN947 secure images under static analysis --- tools/ci-static-analysis-build.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/ci-static-analysis-build.sh b/tools/ci-static-analysis-build.sh index ac629390..1b340d64 100644 --- a/tools/ci-static-analysis-build.sh +++ b/tools/ci-static-analysis-build.sh @@ -1,13 +1,13 @@ #!/bin/sh -# Compile the host and both Armv8-M ports under a static-analysis build tracer. +# Compile the host and every Armv8-M port under a static-analysis build tracer. set -eu make test -for target in stm32h563 mimxrt700; do +for target in stm32h563 mimxrt700 mcxn947; do for engine in native hsm; do case "$target" in - mimxrt700) attest_cose=0 ;; + mimxrt700|mcxn947) attest_cose=0 ;; *) attest_cose=1 ;; esac make TARGET="$target" WT_ENGINE="$engine" \ From b71a211d56ebe963ba23e771b0ca4c29001fb5d5 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Mon, 26 Oct 2026 21:49:53 +0000 Subject: [PATCH 35/45] Check the bytes the MCXN947 ELS RNG host test serves --- tests/host/mcxn947_rng/Makefile | 4 +- tests/host/mcxn947_rng/main.c | 113 +++++++++++++++++++++++++++----- 2 files changed, 97 insertions(+), 20 deletions(-) diff --git a/tests/host/mcxn947_rng/Makefile b/tests/host/mcxn947_rng/Makefile index 716832de..9ca0daab 100644 --- a/tests/host/mcxn947_rng/Makefile +++ b/tests/host/mcxn947_rng/Makefile @@ -27,7 +27,7 @@ CFLAGS := \ -I$(ROOT)/include \ -I$(ROOT)/port/mcxn947 \ -include mock_els.h \ - -std=c99 -O0 -g -Wall -Wextra -pedantic + -std=c99 -O0 -g -Wall -Wextra -pedantic -fno-pie CFLAGS += $(EXTRA_CFLAGS) ALL_OBJS := $(BUILD_DIR)/wt_rng_entropy.o $(BUILD_DIR)/main.o @@ -47,7 +47,7 @@ $(BUILD_DIR)/%.o: %.c mock_els.h | $(BUILD_DIR) $(CC) $(CFLAGS) -c -o $@ $< $(TEST_BIN): $(ALL_OBJS) - $(CC) $(CFLAGS) $(EXTRA_LDFLAGS) -o $@ $(ALL_OBJS) + $(CC) $(CFLAGS) -no-pie $(EXTRA_LDFLAGS) -o $@ $(ALL_OBJS) run: $(TEST_BIN) $(TEST_BIN) diff --git a/tests/host/mcxn947_rng/main.c b/tests/host/mcxn947_rng/main.c index 5a7424e1..9e8154db 100644 --- a/tests/host/mcxn947_rng/main.c +++ b/tests/host/mcxn947_rng/main.c @@ -20,9 +20,10 @@ /* Host suite for the MCXN947 ELS entropy driver over a model of the RND_REQ - * command: a request that misses its deadline must target memory outside the - * stack and must leave the source refusing every later call. The register - * holds 32 bits, so only the low half of a host address is compared. */ + * command: requests serve fresh DMA pages and wipe them, and a request that + * misses its deadline must target memory outside the stack and leave the + * source refusing every later call. Built without PIE so the 32-bit DMA + * register holds a host address of static storage. */ #include #include @@ -32,6 +33,7 @@ #include "wolftrust/spm_transport.h" #define WT_ELS_PAGE_BYTES 64u +#define WT_ELS_PAGE_WORDS_TEST (WT_ELS_PAGE_BYTES / 4u) int wolftrust_rng_generate_block(unsigned char *output, unsigned int sz); @@ -56,18 +58,46 @@ static uint32_t g_requests; static uint32_t g_last_dma_addr; static int g_hang; static uint32_t g_ctrl_seen; +static uintptr_t g_stack_marker; -/* A START seen on the next status read issues the request: it completes at - * once, or with g_hang stays BUSY past the driver's deadline. */ +static int on_stack(uint32_t addr) +{ + uint32_t m = (uint32_t)g_stack_marker; + uint32_t d = (addr > m) ? addr - m : m - addr; + + return d < 0x10000u; +} + +static uint32_t page_word(uint32_t request, uint32_t word) +{ + return 0xA5000000u | (request << 8) | word; +} + +/* A START seen on the next status read issues the request: it fills the page + * at once, or with g_hang stays BUSY past the driver's deadline. */ static void wt_mock_step(void) { + volatile uint32_t *dst; + uint32_t i; + if (g_ctrl != g_ctrl_seen) { if ((g_ctrl & WT_ELS_CTRL_START) != 0u) { /* START self-clears, so an identical later write is a new one. */ g_ctrl &= ~WT_ELS_CTRL_START; g_requests++; g_last_dma_addr = g_dma_addr; - g_status = g_hang ? WT_ELS_STATUS_BUSY : 0u; + if (g_hang) { + g_status = WT_ELS_STATUS_BUSY; + } + else { + g_status = 0u; + if (!on_stack(g_dma_addr)) { + dst = (volatile uint32_t *)(uintptr_t)g_dma_addr; + for (i = 0u; i < g_dma_len / 4u; ++i) { + dst[i] = page_word(g_requests, i); + } + } + } } g_ctrl_seen = g_ctrl; } @@ -96,39 +126,83 @@ int wt_spm_sp_call(struct wt_spm_call* call) return -1; } -static int on_stack(uint32_t addr, const void *marker) +/* out[off..off+len) must hold request's page words starting at word 0. */ +static int served_from(const unsigned char *out, unsigned int len, + uint32_t request) { - uint32_t m = (uint32_t)(uintptr_t)marker; - uint32_t d = (addr > m) ? addr - m : m - addr; + uint32_t w; + unsigned int i; - return d < 0x10000u; + for (i = 0u; i < len; ++i) { + w = page_word(request, i / 4u); + if (out[i] != (unsigned char)(w >> (8u * (i % 4u)))) { + return 0; + } + } + return 1; } -static void test_completed_request(void) +static int page_wiped(void) { - unsigned char out[40]; - uint8_t marker = 0u; + const volatile uint32_t *page = + (const volatile uint32_t *)(uintptr_t)g_last_dma_addr; + uint32_t i; + + for (i = 0u; i < WT_ELS_PAGE_WORDS_TEST; ++i) { + if (page[i] != 0u) { + return 0; + } + } + return 1; +} + +static void test_completed_requests(void) +{ + unsigned char out[100]; + uint32_t first; g_hang = 0; memset(out, 0, sizeof(out)); - EXPECT(wolftrust_rng_generate_block(out, sizeof(out)) == 0, + EXPECT(wolftrust_rng_generate_block(out, 40u) == 0, "a completed RND_REQ serves its words"); EXPECT(g_requests == 1u, "40 bytes take one page"); EXPECT(g_dma_len == WT_ELS_PAGE_BYTES, "a request covers a whole page"); - EXPECT(!on_stack(g_last_dma_addr, &marker), + EXPECT(!on_stack(g_last_dma_addr), "the DMA page is not on the caller's stack"); + EXPECT(served_from(out, 40u, 1u), "the 40 bytes are the page's words"); + EXPECT(page_wiped(), "the page is wiped after the request"); + + first = g_requests; + memset(out, 0, sizeof(out)); + EXPECT(wolftrust_rng_generate_block(out, 16u) == 0, + "a second request succeeds"); + EXPECT(g_requests == first + 1u, "every call issues a fresh request"); + EXPECT(served_from(out, 16u, first + 1u), + "unused words of the earlier page are never served"); + + first = g_requests; + memset(out, 0, sizeof(out)); + EXPECT(wolftrust_rng_generate_block(out, sizeof(out)) == 0, + "a request spanning two pages succeeds"); + EXPECT(g_requests == first + 2u, "100 bytes take two pages"); + EXPECT(served_from(out, WT_ELS_PAGE_BYTES, first + 1u), + "the first 64 bytes are the first page"); + EXPECT(served_from(out + WT_ELS_PAGE_BYTES, + (unsigned int)sizeof(out) - WT_ELS_PAGE_BYTES, + first + 2u), + "the remaining 36 bytes are the second page"); + EXPECT(page_wiped(), "the page is wiped after a multi-page request"); } static void test_late_completion(void) { unsigned char out[16]; - uint8_t marker = 0u; uint32_t before; g_hang = 1; EXPECT(wolftrust_rng_generate_block(out, sizeof(out)) != 0, "a request past its deadline fails"); - EXPECT(!on_stack(g_last_dma_addr, &marker), + EXPECT(!on_stack(g_last_dma_addr), "the timed-out request's DMA target is not on the stack"); /* The request completes after the driver gave up. */ g_status = 0u; @@ -141,7 +215,10 @@ static void test_late_completion(void) int main(void) { - test_completed_request(); + uint8_t marker = 0u; + + g_stack_marker = (uintptr_t)▮ + test_completed_requests(); test_late_completion(); (void)printf("mcxn947 rng: %d checks, %d failures\n", checks, failures); return failures == 0 ? 0 : 1; From d98c1d04bb07310a8d8b16d174a9060ddbcdc32a Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Mon, 26 Oct 2026 21:49:53 +0000 Subject: [PATCH 36/45] Check where guest0 faulted in the MCXN947 hardware negatives --- tests/target/run_mcxn947_hardware.sh | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/tests/target/run_mcxn947_hardware.sh b/tests/target/run_mcxn947_hardware.sh index 3205f649..ca0e699b 100755 --- a/tests/target/run_mcxn947_hardware.sh +++ b/tests/target/run_mcxn947_hardware.sh @@ -63,6 +63,10 @@ flash_sector=0x2000 guest0_ram=0x20000000 guest1_ram=0x20018000 probe_addr=0x20028000 +# What guest0's negatives touch (tests/firmware/mcxn947-baremetal/Makefile): +# the SPM's ELS through its NS alias, and Secure RAM for the restart probe. +periph_probe_addr=0x40054000 +restart_probe_addr=0x30031000 handoff_addr=0x30030000 syscon_lc_state=0x50000980 # The guests' manifest restart budget (port/mcxn947/partitions.c). @@ -392,12 +396,16 @@ check_secure_side() { "wolfBoot verified the wolfTrust image signature (console)" } -# guest0 faulted on every launch: the monitor spent its restart budget and -# quarantined it, while guest1 ran untouched. +# guest0 faulted at $1 on every launch (the Secure fault handler records the +# address), so a fault anywhere else fails the scenario; the monitor spent its +# restart budget and quarantined guest0, while guest1 ran untouched. check_guest0_quarantined() { - local restarts quarantines st beat + local want="$1" restarts quarantines st beat fault restarts="$(sym_word g_wt_restart_events)" quarantines="$(sym_word g_wt_quarantine_events)" + fault="$(sym_word g_last_fault_address)" + check "$([ "$((0x$fault))" -eq "$((want))" ]; echo $?)" \ + "guest0's last fault was its probe's access to $want (0x$fault)" check "$([ "$((0x$restarts))" -eq "$restart_limit" ]; echo $?)" \ "guest0 relaunched through its restart budget ($((0x$restarts)) of $restart_limit)" check "$([ "$((0x$quarantines))" -eq 1 ]; echo $?)" \ @@ -439,7 +447,7 @@ positive) ;; sauneg) run_chain "$baremetal_dir" "WT_SAU_PROBE=1" - check_guest0_quarantined + check_guest0_quarantined "$probe_addr" peer="$(word_at "$probe_addr" 0)" check "$([ "$peer" != "deadbeef" ]; echo $?)" \ "guest1 RAM never received guest0's sentinel (0x$peer)" @@ -448,7 +456,7 @@ sauneg) ;; periphneg) run_chain "$baremetal_dir" "WT_PERIPH_NEG_PROBE=1" - check_guest0_quarantined + check_guest0_quarantined "$periph_probe_addr" check "$(! grep -q "periph probe LEAKED" "$evidence/console.log"; echo $?)" \ "no NS read of the SPM's ELS ever returned (console)" check_secure_side @@ -456,7 +464,7 @@ periphneg) ;; restart) run_chain "$baremetal_dir" "WT_GUEST_FAULT_PROBE=1" - check_guest0_quarantined + check_guest0_quarantined "$restart_probe_addr" check_secure_side log "PASS: hardware/mcxn947/$scenario" ;; From e834a46d64f692c359386ae8212136d299e1169e Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Mon, 26 Oct 2026 22:05:36 +0000 Subject: [PATCH 37/45] Release the sticky MCXN947 code cache clear after flash writes --- port/mcxn947/rom_flash.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/port/mcxn947/rom_flash.c b/port/mcxn947/rom_flash.c index 5214063a..e2a6e3a9 100644 --- a/port/mcxn947/rom_flash.c +++ b/port/mcxn947/rom_flash.c @@ -111,8 +111,10 @@ static void wt_rom_flash_irq_restore(uint32_t primask) static void wt_rom_flash_cache_clear(void) { #if defined(__ARM_EABI__) + /* CLR_LPCAC is sticky: the cache stays cleared until it is written 0. */ WT_SYSCON_LPCAC_CTRL |= WT_SYSCON_LPCAC_CTRL_CLR; __asm volatile("dsb sy\n\tisb" ::: "memory"); + WT_SYSCON_LPCAC_CTRL &= ~WT_SYSCON_LPCAC_CTRL_CLR; #endif } From f03664279ee971998796006ad0db6d5509b3cae4 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Mon, 26 Oct 2026 22:05:36 +0000 Subject: [PATCH 38/45] Bound the FWU probe tail to its write payload --- tests/firmware/psa-guest/main.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/firmware/psa-guest/main.c b/tests/firmware/psa-guest/main.c index 311c6a3c..083f076d 100644 --- a/tests/firmware/psa-guest/main.c +++ b/tests/firmware/psa-guest/main.c @@ -851,6 +851,9 @@ static void exercise_hsm_attack_probe(void) #if !defined(WT_FWU_PROBE_TAIL_BYTES) #define WT_FWU_PROBE_TAIL_BYTES 32u #endif +#if WT_FWU_PROBE_TAIL_BYTES < 1 || WT_FWU_PROBE_TAIL_BYTES > 512 +#error "WT_FWU_PROBE_TAIL_BYTES must fit the probe's 512-byte write payload" +#endif #define GUEST_FWU_OP_START 2u #define GUEST_FWU_OP_WRITE 3u #define GUEST_FWU_OP_FINISH 4u From 8e4257a45c36c86d495ef85b4e9bc6d810654e93 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Mon, 26 Oct 2026 22:05:36 +0000 Subject: [PATCH 39/45] List the MCXN947 smoke scenarios without confboot --- .github/workflows/README.md | 2 +- docs/Testing.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index b1ccd428..c37af9a8 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -21,7 +21,7 @@ An accepted upload queues analysis; results appear after Coverity processes it. | Tier | Trigger | Purpose | |------|---------|---------| | **Fast (per-PR)** | every PR; push to main | host unit suites, ISO C99, house style, bare-scope scan, Arm PSA-FF conformance, cross-compile, compiler matrix, sanitizers, valgrind, integrations, core/port split guard | -| **M33MU smoke** | every PR | per port, on both crypto engines: STM32H563 `positive gtzcneg crossdomain bothpsa confboot devcrypto`; MIMXRT700 `positive ahbscneg crossdomain bothpsa confboot devcrypto`; MCXN947 `positive sauneg crossdomain bothpsa confboot devcrypto` | +| **M33MU smoke** | every PR | per port, on both crypto engines: STM32H563 `positive gtzcneg crossdomain bothpsa confboot devcrypto`; MIMXRT700 `positive ahbscneg crossdomain bothpsa confboot devcrypto`; MCXN947 `positive sauneg crossdomain bothpsa devcrypto` (no `confboot`, see `docs/MCXN947-Guide.md`) | | **M33MU full** | PR labels `ci:all`, `ci:h5`, `ci:rt700`, `ci:mcxn947`; push to main; `cron: 0 8 * * *`; `workflow_dispatch` (port input) | every scenario of that port on both engines (see below) | The M33MU workflow (`m33mu.yml`) is label-selected the way wolfProvider's diff --git a/docs/Testing.md b/docs/Testing.md index 177a9e34..8e2d69ba 100644 --- a/docs/Testing.md +++ b/docs/Testing.md @@ -577,8 +577,8 @@ groups) is tiered. Every pull request runs each port's smoke tier on both crypto engines (STM32H563: `positive`, `gtzcneg`, `crossdomain`, `bothpsa`, `confboot`, `devcrypto`; MIMXRT700: `positive`, `ahbscneg`, `crossdomain`, `bothpsa`, `confboot`, `devcrypto`; -MCXN947: `positive`, `sauneg`, `crossdomain`, `bothpsa`, `confboot`, -`devcrypto`). +MCXN947: `positive`, `sauneg`, `crossdomain`, `bothpsa`, `devcrypto`; its +`confboot` is not claimed on M33MU, see the MCXN947 Guide). The full matrix runs on a push to `main`, on the nightly schedule, on manual dispatch (with a `port` input), and on a pull request that carries the From e4e146980fe038fbebe24d2c51424186553ae2ac Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Mon, 26 Oct 2026 22:18:31 +0000 Subject: [PATCH 40/45] Cover the MCXN947 ELS locked and failed request refusals --- tests/host/mcxn947_rng/Makefile | 10 +++-- tests/host/mcxn947_rng/main.c | 67 ++++++++++++++++++++++++++++++--- 2 files changed, 68 insertions(+), 9 deletions(-) diff --git a/tests/host/mcxn947_rng/Makefile b/tests/host/mcxn947_rng/Makefile index 9ca0daab..e2bdbbd4 100644 --- a/tests/host/mcxn947_rng/Makefile +++ b/tests/host/mcxn947_rng/Makefile @@ -32,6 +32,8 @@ CFLAGS += $(EXTRA_CFLAGS) ALL_OBJS := $(BUILD_DIR)/wt_rng_entropy.o $(BUILD_DIR)/main.o TEST_BIN := $(BUILD_DIR)/test_mcxn947_rng +# The driver latches failure, so each case runs in its own process. +CASES := default locked error .PHONY: all run compilers sanitize valgrind clean @@ -50,7 +52,7 @@ $(TEST_BIN): $(ALL_OBJS) $(CC) $(CFLAGS) -no-pie $(EXTRA_LDFLAGS) -o $@ $(ALL_OBJS) run: $(TEST_BIN) - $(TEST_BIN) + for c in $(CASES); do $(TEST_BIN) $$c || exit 1; done compilers: $(MAKE) clean run CC=gcc BUILD_DIR=$(BUILD_DIR)/gcc @@ -63,8 +65,10 @@ sanitize: EXTRA_LDFLAGS="-fsanitize=address,undefined" valgrind: clean $(TEST_BIN) - valgrind --error-exitcode=1 --leak-check=full --show-leak-kinds=all \ - $(TEST_BIN) + for c in $(CASES); do \ + valgrind --error-exitcode=1 --leak-check=full \ + --show-leak-kinds=all $(TEST_BIN) $$c || exit 1; \ + done clean: rm -rf $(BUILD_DIR) diff --git a/tests/host/mcxn947_rng/main.c b/tests/host/mcxn947_rng/main.c index 9e8154db..f436c836 100644 --- a/tests/host/mcxn947_rng/main.c +++ b/tests/host/mcxn947_rng/main.c @@ -22,8 +22,10 @@ /* Host suite for the MCXN947 ELS entropy driver over a model of the RND_REQ * command: requests serve fresh DMA pages and wipe them, and a request that * misses its deadline must target memory outside the stack and leave the - * source refusing every later call. Built without PIE so the 32-bit DMA - * register holds a host address of static storage. */ + * source refusing every later call, as must a request the engine fails; a + * locked engine refuses before any request. Each case runs in its own process + * (the driver latches failure). Built without PIE so the 32-bit DMA register + * holds a host address of static storage. */ #include #include @@ -36,6 +38,7 @@ #define WT_ELS_PAGE_WORDS_TEST (WT_ELS_PAGE_BYTES / 4u) int wolftrust_rng_generate_block(unsigned char *output, unsigned int sz); +extern volatile uint32_t g_wt_els_errors; static int checks; static int failures; @@ -52,6 +55,8 @@ static int failures; static volatile uint32_t g_status; static volatile uint32_t g_ctrl; static volatile uint32_t g_scratch; +static volatile uint32_t g_err_clr; +static int g_fail_request; static uint32_t g_dma_addr; static uint32_t g_dma_len; static uint32_t g_requests; @@ -89,6 +94,9 @@ static void wt_mock_step(void) if (g_hang) { g_status = WT_ELS_STATUS_BUSY; } + else if (g_fail_request) { + g_status = WT_ELS_STATUS_ERR; + } else { g_status = 0u; if (!on_stack(g_dma_addr)) { @@ -115,6 +123,8 @@ volatile uint32_t* wt_mock_reg(uintptr_t address) return (volatile uint32_t *)&g_dma_addr; case WT_ELS_BASE_S + 0x3Cu: return (volatile uint32_t *)&g_dma_len; + case WT_ELS_BASE_S + 0x50u: + return &g_err_clr; default: return &g_scratch; } @@ -213,13 +223,58 @@ static void test_late_completion(void) EXPECT(g_requests == before, "no new request is issued after a timeout"); } -int main(void) +static void test_locked_engine(void) +{ + unsigned char out[16]; + + g_status = WT_ELS_STATUS_LOCKED; + EXPECT(wolftrust_rng_generate_block(out, sizeof(out)) != 0, + "a locked engine refuses"); + EXPECT(g_requests == 0u, "a locked engine is never sent a request"); + EXPECT(g_wt_els_errors == 1u, "the refusal is counted"); + g_status = 0u; + EXPECT(wolftrust_rng_generate_block(out, sizeof(out)) == 0, + "the engine serves once it is no longer locked"); + EXPECT(served_from(out, sizeof(out), 1u), "and serves its page"); +} + +static void test_failed_request(void) +{ + unsigned char out[16]; + uint32_t before; + + g_fail_request = 1; + EXPECT(wolftrust_rng_generate_block(out, sizeof(out)) != 0, + "a request the engine fails is refused"); + EXPECT(g_requests == 1u, "the failed request was issued"); + EXPECT(g_wt_els_errors == 1u, "the failure is counted"); + EXPECT((g_err_clr & WT_ELS_ERR_STATUS_CLR_ALL) != 0u, + "the error status is cleared"); + g_fail_request = 0; + g_status = 0u; + before = g_requests; + EXPECT(wolftrust_rng_generate_block(out, sizeof(out)) != 0, + "the source refuses every call after a failed request"); + EXPECT(g_requests == before, "no new request is issued after a failure"); +} + +int main(int argc, char **argv) { uint8_t marker = 0u; + const char *which = (argc > 1) ? argv[1] : "default"; g_stack_marker = (uintptr_t)▮ - test_completed_requests(); - test_late_completion(); - (void)printf("mcxn947 rng: %d checks, %d failures\n", checks, failures); + if (strcmp(which, "locked") == 0) { + test_locked_engine(); + } + else if (strcmp(which, "error") == 0) { + test_failed_request(); + } + else { + test_completed_requests(); + test_late_completion(); + } + (void)printf("mcxn947 rng %s: %d checks, %d failures\n", which, checks, + failures); return failures == 0 ? 0 : 1; } From 6a81d2970a202f216fc7b08787d3863445f390ef Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Mon, 26 Oct 2026 23:28:20 +0000 Subject: [PATCH 41/45] Carry the M33MU decode of post-indexed LDRSH for the MCXN947 model --- tests/target/m33mu-mcxn947.patch | 172 +++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) diff --git a/tests/target/m33mu-mcxn947.patch b/tests/target/m33mu-mcxn947.patch index 258b3b9c..4049df45 100644 --- a/tests/target/m33mu-mcxn947.patch +++ b/tests/target/m33mu-mcxn947.patch @@ -355,6 +355,131 @@ index 148ef22..d15bd66 100644 } mm_bool mm_mcxn947_secure_rom_call_allowed(enum mm_sec_state sec) +diff --git a/include/m33mu/decode.h b/include/m33mu/decode.h +index 9f5739c..382fce2 100644 +--- a/include/m33mu/decode.h ++++ b/include/m33mu/decode.h +@@ -124,6 +124,8 @@ enum mm_op_kind { + MM_OP_LDRSB_POST_IMM, + MM_OP_LDRSH_REG, + MM_OP_LDRSH_IMM, ++ MM_OP_LDRSH_PRE_IMM, ++ MM_OP_LDRSH_POST_IMM, + MM_OP_LDRSHT, + MM_OP_STRHT, + MM_OP_LDRHT, +diff --git a/src/execute.c b/src/execute.c +index 9f06d51..aaddc0f 100644 +--- a/src/execute.c ++++ b/src/execute.c +@@ -4492,6 +4492,36 @@ enum mm_exec_status mm_execute_decoded(struct mm_execute_ctx *ctx) + cpu.r[d.rn] = base + d.imm; + } + } break; ++ case MM_OP_LDRSH_POST_IMM: { ++ mm_u32 base = cpu.r[d.rn]; ++ mm_u32 val = 0; ++ EXEC_CHECK_UNALIGN(base, 2u); ++ if (!exec_read_signed(&map, cpu.sec_state, base, 2u, &val)) { ++ if (!raise_mem_fault(&cpu, &map, &scs, f.pc_fetch, cpu.xpsr, base, MM_FALSE)) done = MM_TRUE; ++ return MM_EXEC_CONTINUE; ++ } ++ cpu.r[d.rd] = val; ++ if (d.rn == 13u) { ++ EXEC_SET_SP(base + d.imm); ++ } else { ++ cpu.r[d.rn] = base + d.imm; ++ } ++ } break; ++ case MM_OP_LDRSH_PRE_IMM: { ++ mm_u32 addr = cpu.r[d.rn] + d.imm; ++ mm_u32 val = 0; ++ EXEC_CHECK_UNALIGN(addr, 2u); ++ if (!exec_read_signed(&map, cpu.sec_state, addr, 2u, &val)) { ++ if (!raise_mem_fault(&cpu, &map, &scs, f.pc_fetch, cpu.xpsr, addr, MM_FALSE)) done = MM_TRUE; ++ return MM_EXEC_CONTINUE; ++ } ++ cpu.r[d.rd] = val; ++ if (d.rn == 13u) { ++ EXEC_SET_SP(addr); ++ } else { ++ cpu.r[d.rn] = addr; ++ } ++ } break; + case MM_OP_LDRSH_IMM: + case MM_OP_LDRSHT: { + mm_u32 addr = cpu.r[d.rn] + d.imm; +diff --git a/src/m33mu/capstone.c b/src/m33mu/capstone.c +index bbfad17..b401d68 100644 +--- a/src/m33mu/capstone.c ++++ b/src/m33mu/capstone.c +@@ -550,7 +550,8 @@ static mm_bool cross_check_kind(const cs_insn *insn, const struct mm_decoded *de + case ARM_INS_LDRSB: return (dec->kind == MM_OP_LDRSB_IMM || + dec->kind == MM_OP_LDRSB_POST_IMM || + dec->kind == MM_OP_LDRSB_REG); +- case ARM_INS_LDRSH: return (dec->kind == MM_OP_LDRSH_IMM || dec->kind == MM_OP_LDRSH_REG); ++ case ARM_INS_LDRSH: return (dec->kind == MM_OP_LDRSH_IMM || dec->kind == MM_OP_LDRSH_REG || ++ dec->kind == MM_OP_LDRSH_POST_IMM || dec->kind == MM_OP_LDRSH_PRE_IMM); + case ARM_INS_DSB: return dec->kind == MM_OP_DSB; + case ARM_INS_DMB: return dec->kind == MM_OP_DMB; + case ARM_INS_ISB: return dec->kind == MM_OP_ISB; +@@ -1288,12 +1289,13 @@ static mm_bool cross_check_operands(const struct mm_fetch_result *fetch, const c + if (dec->kind == MM_OP_LDR_POST_IMM || dec->kind == MM_OP_STR_POST_IMM || + dec->kind == MM_OP_LDRB_POST_IMM || dec->kind == MM_OP_STRB_POST_IMM || + dec->kind == MM_OP_LDRH_POST_IMM || dec->kind == MM_OP_STRH_POST_IMM || +- dec->kind == MM_OP_LDRSB_POST_IMM) { ++ dec->kind == MM_OP_LDRSB_POST_IMM || dec->kind == MM_OP_LDRSH_POST_IMM) { + expect_wb = MM_TRUE; + expect_post = MM_TRUE; + } else if (dec->kind == MM_OP_LDR_PRE_IMM || dec->kind == MM_OP_STR_PRE_IMM || + dec->kind == MM_OP_LDRB_PRE_IMM || dec->kind == MM_OP_STRB_PRE_IMM || +- dec->kind == MM_OP_LDRH_PRE_IMM || dec->kind == MM_OP_STRH_PRE_IMM) { ++ dec->kind == MM_OP_LDRH_PRE_IMM || dec->kind == MM_OP_STRH_PRE_IMM || ++ dec->kind == MM_OP_LDRSH_PRE_IMM) { + expect_wb = MM_TRUE; + expect_post = MM_FALSE; + } +diff --git a/src/m33mu/decode.c b/src/m33mu/decode.c +index 6882d58..0f5eed2 100644 +--- a/src/m33mu/decode.c ++++ b/src/m33mu/decode.c +@@ -1564,6 +1564,36 @@ static struct mm_decoded decode_32(mm_u32 insn) + d.undefined = MM_FALSE; + return d; + } ++ /* Post-index writeback form: LDRSH Rt, [Rn], #+/-imm8 */ ++ if (p == 0u && w == 1u) { ++ mm_u8 rn = (mm_u8)((insn >> 16) & 0x0fu); ++ mm_u8 rt = (mm_u8)((insn >> 12) & 0x0fu); ++ mm_u32 imm8 = insn & 0xffu; ++ if (rt == 15u || rn == 15u) { ++ return d; ++ } ++ d.kind = MM_OP_LDRSH_POST_IMM; ++ d.rn = rn; ++ d.rd = rt; ++ d.imm = u ? imm8 : (mm_u32)(0u - imm8); ++ d.undefined = MM_FALSE; ++ return d; ++ } ++ /* Pre-index writeback form: LDRSH Rt, [Rn, #+/-imm8]! */ ++ if (p == 1u && w == 1u) { ++ mm_u8 rn = (mm_u8)((insn >> 16) & 0x0fu); ++ mm_u8 rt = (mm_u8)((insn >> 12) & 0x0fu); ++ mm_u32 imm8 = insn & 0xffu; ++ if (rt == 15u || rn == 15u) { ++ return d; ++ } ++ d.kind = MM_OP_LDRSH_PRE_IMM; ++ d.rn = rn; ++ d.rd = rt; ++ d.imm = u ? imm8 : (mm_u32)(0u - imm8); ++ d.undefined = MM_FALSE; ++ return d; ++ } + } + + /* LDAEX (word) */ diff --git a/src/main.c b/src/main.c index aad0697..9457fa4 100644 --- a/src/main.c @@ -373,3 +498,50 @@ index aad0697..9457fa4 100644 } if (cpu_name != 0 && strcmp(cpu_name, "lpc55s69") == 0) { /* LPC55S69 ROM is at 0x03000000 (NS alias) / 0x13000000 (S alias). +diff --git a/tests/decode_phase3_test.c b/tests/decode_phase3_test.c +index 2731177..4564284 100644 +--- a/tests/decode_phase3_test.c ++++ b/tests/decode_phase3_test.c +@@ -293,6 +293,33 @@ static int test_ldrsh_w_imm8_pre(void) + return 0; + } + ++static int test_ldrsh_w_imm8_post(void) ++{ ++ struct mm_decoded dec; ++ /* LDRSH.W fp, [r8], #2 : hw1=0xf938, hw2=0xbb02 (P=0,U=1,W=1) */ ++ if (decode_insn(0xf938u, 0xbb02u, &dec) != 0) return 1; ++ if (dec.kind != MM_OP_LDRSH_POST_IMM) return 1; ++ if (dec.rd != 11u || dec.rn != 8u) return 1; ++ if (dec.imm != 2u) return 1; ++ /* LDRSH.W r1, [r2], #-4 : hw1=0xf932, hw2=0x1904 (P=0,U=0,W=1) */ ++ if (decode_insn(0xf932u, 0x1904u, &dec) != 0) return 1; ++ if (dec.kind != MM_OP_LDRSH_POST_IMM) return 1; ++ if (dec.rd != 1u || dec.rn != 2u) return 1; ++ if (dec.imm != 0xfffffffcu) return 1; ++ return 0; ++} ++ ++static int test_ldrsh_w_imm8_pre_wb(void) ++{ ++ struct mm_decoded dec; ++ /* LDRSH.W r3, [r4, #0x20]! : hw1=0xf934, hw2=0x3f20 (P=1,U=1,W=1) */ ++ if (decode_insn(0xf934u, 0x3f20u, &dec) != 0) return 1; ++ if (dec.kind != MM_OP_LDRSH_PRE_IMM) return 1; ++ if (dec.rd != 3u || dec.rn != 4u) return 1; ++ if (dec.imm != 0x20u) return 1; ++ return 0; ++} ++ + static int test_clz_w_decode(void) + { + struct mm_decoded dec; +@@ -604,6 +631,8 @@ int main(void) + { "ldrsb_w_imm8_pre", test_ldrsb_w_imm8_pre }, + { "ldrsh_w_imm12", test_ldrsh_w_imm12 }, + { "ldrsh_w_imm8_pre", test_ldrsh_w_imm8_pre }, ++ { "ldrsh_w_imm8_post", test_ldrsh_w_imm8_post }, ++ { "ldrsh_w_imm8_pre_wb", test_ldrsh_w_imm8_pre_wb }, + { "clz_w_decode", test_clz_w_decode }, + { "rbit_w_decode", test_rbit_w_decode }, + { "tt_decode", test_tt_decode }, From fe84b635f727d783c2be81e59d35e4d8aa63662e Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Mon, 26 Oct 2026 23:28:21 +0000 Subject: [PATCH 42/45] Run the FF-M IPC conformance suite on the MCXN947 model --- .github/workflows/README.md | 2 +- docs/MCXN947-Guide.md | 14 +++++--------- docs/Testing.md | 9 +++++---- tests/target/lib/scenario_matrix.py | 4 +++- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index c37af9a8..b1ccd428 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -21,7 +21,7 @@ An accepted upload queues analysis; results appear after Coverity processes it. | Tier | Trigger | Purpose | |------|---------|---------| | **Fast (per-PR)** | every PR; push to main | host unit suites, ISO C99, house style, bare-scope scan, Arm PSA-FF conformance, cross-compile, compiler matrix, sanitizers, valgrind, integrations, core/port split guard | -| **M33MU smoke** | every PR | per port, on both crypto engines: STM32H563 `positive gtzcneg crossdomain bothpsa confboot devcrypto`; MIMXRT700 `positive ahbscneg crossdomain bothpsa confboot devcrypto`; MCXN947 `positive sauneg crossdomain bothpsa devcrypto` (no `confboot`, see `docs/MCXN947-Guide.md`) | +| **M33MU smoke** | every PR | per port, on both crypto engines: STM32H563 `positive gtzcneg crossdomain bothpsa confboot devcrypto`; MIMXRT700 `positive ahbscneg crossdomain bothpsa confboot devcrypto`; MCXN947 `positive sauneg crossdomain bothpsa confboot devcrypto` | | **M33MU full** | PR labels `ci:all`, `ci:h5`, `ci:rt700`, `ci:mcxn947`; push to main; `cron: 0 8 * * *`; `workflow_dispatch` (port input) | every scenario of that port on both engines (see below) | The M33MU workflow (`m33mu.yml`) is label-selected the way wolfProvider's diff --git a/docs/MCXN947-Guide.md b/docs/MCXN947-Guide.md index d04fe3b3..f6b36f7d 100644 --- a/docs/MCXN947-Guide.md +++ b/docs/MCXN947-Guide.md @@ -37,15 +37,11 @@ the part; this port rehearses life cycles and writes none of them. restart budget while the peer keeps running, and every other scenario the matrix lists for this port (`tests/target/lib/scenario_matrix.py`: the whole MIMXRT700 set, every isolation level 3 scenario included) passes on - the model on both crypto engines. The one MIMXRT700 scenario not listed is - `confboot`: on the M33MU commit this port pins (the one with the MCXN947 - RAM attribution fix), the Arm FF-M IPC suite stops in test i002 check 5, - the Non-secure val guest parked in its HardFault handler with an - undefined-instruction UsageFault right after `psa_connect` returns, and - the MIMXRT700 chain does the same on that emulator commit while it passes - on the older one its runner pins; the dev_apis storage, crypto and - attestation suites and the vault recovery runs complete on both. It is an - emulator regression to bisect upstream, not a port claim. + the model on both crypto engines. That includes `confboot`, Arm's + unmodified FF-M IPC suite (85 passed, 4 skipped, as on the MIMXRT700): its + val client, built with GCC 12, uses the post-indexed `LDRSH` (immediate) + form, which the carried M33MU patch decodes (the pinned emulator commit + took it as undefined). - **Not validated:** any guest flash fence and any life cycle write. The TRDC and the AHB secure controller are left as the first loader leaves them. See [Open items](#open-items). diff --git a/docs/Testing.md b/docs/Testing.md index 8e2d69ba..68ff8c76 100644 --- a/docs/Testing.md +++ b/docs/Testing.md @@ -397,8 +397,9 @@ recorded separately. M33MU also models the MCXN947 (`--cpu mcxn947`: internal flash, the boot ROM flash API tree, and, with `tests/target/m33mu-mcxn947.patch`, the ELS `RND_REQ` register model behind its real clock gate, the ROM's page-based -bit-clearing flash program, an executable ROM, and the OEM_OPEN life cycle -in SYSCON), so the MCXN947 port has the same emulator gate as the MIMXRT700, +bit-clearing flash program, an executable ROM, the OEM_OPEN life cycle in +SYSCON, and the post-indexed `LDRSH` (immediate) decode the conformance +client needs), so the MCXN947 port has the same emulator gate as the MIMXRT700, with the same scenario names: ```sh @@ -577,8 +578,8 @@ groups) is tiered. Every pull request runs each port's smoke tier on both crypto engines (STM32H563: `positive`, `gtzcneg`, `crossdomain`, `bothpsa`, `confboot`, `devcrypto`; MIMXRT700: `positive`, `ahbscneg`, `crossdomain`, `bothpsa`, `confboot`, `devcrypto`; -MCXN947: `positive`, `sauneg`, `crossdomain`, `bothpsa`, `devcrypto`; its -`confboot` is not claimed on M33MU, see the MCXN947 Guide). +MCXN947: `positive`, `sauneg`, `crossdomain`, `bothpsa`, `confboot`, +`devcrypto`). The full matrix runs on a push to `main`, on the nightly schedule, on manual dispatch (with a `port` input), and on a pull request that carries the diff --git a/tests/target/lib/scenario_matrix.py b/tests/target/lib/scenario_matrix.py index 744f7b1a..f5feb48b 100755 --- a/tests/target/lib/scenario_matrix.py +++ b/tests/target/lib/scenario_matrix.py @@ -162,7 +162,8 @@ "label": "ci:mcxn947", "arm": "mcxn947-m33mu", "image": "ghcr.io/wolfssl/wolfboot-ci-m33mu:v1.25", - "smoke": ("positive", "sauneg", "crossdomain", "bothpsa", "devcrypto"), + "smoke": ("positive", "sauneg", "crossdomain", "bothpsa", "confboot", + "devcrypto"), "groups": ( ("positive", "MCXN947 positive lifecycle (SAU guest windows)"), ("sauneg", "MCXN947 cross-guest store faults and is contained"), @@ -178,6 +179,7 @@ "MCXN947 attestation negatives and FWU staging"), ("hsmattackneg", "MCXN947 wolfHSM cross-namespace + NVM relay negatives"), + ("confboot", "MCXN947 FF-M IPC conformance (85/4)"), ("devstorage devattest devattestqcbor", "MCXN947 dev_apis storage and attestation conformance"), ("devcrypto vaultrecover vaultrecoversec", From 2e2698a9dbddc14ef697b9bca4e5636c898520c5 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Mon, 26 Oct 2026 23:28:21 +0000 Subject: [PATCH 43/45] Prove a fence-required MCXN947 build launches no guest --- docs/MCXN947-Guide.md | 15 ++++++++++----- tests/target/lib/scenario_matrix.py | 1 + tests/target/run_mcxn947_m33mu.sh | 7 ++++++- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/docs/MCXN947-Guide.md b/docs/MCXN947-Guide.md index f6b36f7d..62a6c589 100644 --- a/docs/MCXN947-Guide.md +++ b/docs/MCXN947-Guide.md @@ -411,11 +411,16 @@ fuse map and that every ladder step only sets fuse bits. under it). - **Guest flash fence.** No hardware write protection of the guest windows is claimed: `wt_platform_guest_flash_wrp_ok` reports every window unprotected, - so a `WT_GUEST_FLASH_WRP=1` image refuses every guest. The TRDC MBC gates - bus accesses to the flash, not the FMU's program and erase commands, so it - is not the fence; the candidate is a flash page lock the ROM's - `kStatus_FLASH_ModifyProtectedAreaDisallowed` status names, which needs the - reference manual and a silicon negative before it can be claimed. + so a `WT_GUEST_FLASH_WRP=1` image refuses every guest (the `wrpoff` + scenario proves it). The part offers no reset-locked equivalent of the + MIMXRT700's XSPI fence: the FMU has no region or lock registers and takes + program and erase as commands; the TRDC MBC's access policies lock until + reset, but which policy a flash block uses has no lock, so Secure code can + move a block back to a writable one; and the ROM's flash firewall + (`kStatus_FLASH_ModifyProtectedAreaDisallowed`) follows the OTP + `ACTIVE_IMG_PROT` setting, which is permanent and belongs to the lock flow. + A peer guest reaches the flash only through the FMU or the Secure ROM, so + the open question is whether to claim that attribution as the fence. - **Other bus masters.** CPU1, DMA, and the ELS DMA are not fenced from Secure RAM; programming the TRDC and the AHB secure controller for them is silicon work that M33MU does not model. diff --git a/tests/target/lib/scenario_matrix.py b/tests/target/lib/scenario_matrix.py index f5feb48b..ccdf7750 100755 --- a/tests/target/lib/scenario_matrix.py +++ b/tests/target/lib/scenario_matrix.py @@ -168,6 +168,7 @@ ("positive", "MCXN947 positive lifecycle (SAU guest windows)"), ("sauneg", "MCXN947 cross-guest store faults and is contained"), ("restart authneg", "MCXN947 guest restart budget and launch refusal"), + ("wrpoff", "MCXN947 fence-required build launches no guest"), ("rollbackneg remeasureneg manifestneg spbudgetneg", "MCXN947 secure verdict negatives"), ("crossdomain keystoreneg", "MCXN947 SP domain isolation negatives"), diff --git a/tests/target/run_mcxn947_m33mu.sh b/tests/target/run_mcxn947_m33mu.sh index 767816c5..abdf94cc 100755 --- a/tests/target/run_mcxn947_m33mu.sh +++ b/tests/target/run_mcxn947_m33mu.sh @@ -20,7 +20,8 @@ # restarts it in place while both guests finish; fpneg proves only that the # relay's FP instruction takes a NOCP UsageFault. restart spends guest0's # restart budget on a launch-time fault; authneg refuses a tampered guest0 at -# launch. guest1 runs on through all of them. +# launch. guest1 runs on through all of them. wrpoff builds wolfTrust to +# require a guest flash fence this port does not have, so neither guest runs. # # bothpsa, bothiso, attestneg, hsmattackneg, and fwustage run the portable # PSA test guest (tests/firmware/psa-guest) in both windows; hsmattackneg @@ -438,6 +439,10 @@ case "$scenario" in check "$([ "$sau_refusals" -eq 1 ]; echo $?)" \ "the refusal came from the SAU" ;; + wrpoff) + refute_re "no guest entered its domain without a guest flash fence" \ + "wolfTrust MCXN947 guest[01]: start" + ;; authneg) refute_re "the tampered guest0 never entered its domain" \ "wolfTrust MCXN947 guest0: start" From 69348da54fd3f056ee16033d840904104419c87a Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Mon, 26 Oct 2026 23:35:59 +0000 Subject: [PATCH 44/45] Run the fence-required MCXN947 build on the board --- docs/MCXN947-Guide.md | 7 ++++-- docs/Testing.md | 2 +- tests/target/run_mcxn947_hardware.sh | 33 ++++++++++++++++++++++++---- tests/target/run_mcxn947_suite.sh | 2 +- 4 files changed, 36 insertions(+), 8 deletions(-) diff --git a/docs/MCXN947-Guide.md b/docs/MCXN947-Guide.md index 62a6c589..e3dc5247 100644 --- a/docs/MCXN947-Guide.md +++ b/docs/MCXN947-Guide.md @@ -271,7 +271,7 @@ the SAU with the peer window Secure at dispatch. ## Build and verify on the FRDM-MCXN947 ```sh -make test-hardware TARGET=mcxn947 # positive sauneg periphneg restart psa +make test-hardware TARGET=mcxn947 # positive sauneg periphneg restart psa wrpoff tests/target/run_mcxn947_hardware.sh positive WT_ENGINE=hsm tests/target/run_mcxn947_hardware.sh positive ``` @@ -297,7 +297,10 @@ compares it, and asserts the result over SWD: `0xFF`, the handoff record consumed, IPSR outside the fault handlers, and wolfBoot's `Verifying signature...done` on the console; - for `psa`, each PSA guest's mailbox stage bits (TEE, crypto, ITS, PS, - key-ops, SHA-256 KAT, COSE attestation, done). + key-ops, SHA-256 KAT, COSE attestation, done); +- for `wrpoff`, a Secure image built with `WT_GUEST_FLASH_WRP=1` refusing + both guests (launch refused mask `0x3`, both mailboxes untouched), since + the port reports no guest flash fence. Each run's console capture, image readbacks and an SWD dump of every word the checks read are kept under `logs/mcxn947-hw/-/` diff --git a/docs/Testing.md b/docs/Testing.md index 68ff8c76..ef3b918b 100644 --- a/docs/Testing.md +++ b/docs/Testing.md @@ -431,7 +431,7 @@ separately. `make test-hardware TARGET=mcxn947` runs `tests/target/run_mcxn947_suite.sh` on the host that owns the FRDM-MCXN947's MCU-Link: `positive`, `sauneg`, -`periphneg`, `restart`, and `psa` by default (`WT_MCXN947_SCENARIOS` +`periphneg`, `restart`, `psa`, and `wrpoff` by default (`WT_MCXN947_SCENARIOS` overrides the list; `WT_ENGINE` selects the crypto engine), each built, flashed at its physical offset, erased to a fresh vault, read back, and asserted over the debug port (`tests/target/run_mcxn947_hardware.sh`; the diff --git a/tests/target/run_mcxn947_hardware.sh b/tests/target/run_mcxn947_hardware.sh index ca0e699b..18ac15f6 100755 --- a/tests/target/run_mcxn947_hardware.sh +++ b/tests/target/run_mcxn947_hardware.sh @@ -25,6 +25,9 @@ # psa both Non-secure windows run the portable PSA client # (tests/firmware/psa-guest): every lifecycle stage must complete # in both mailboxes, with the ELS behind psa_generate_random. +# wrpoff wolfTrust built with WT_GUEST_FLASH_WRP=1 requires a guest flash +# fence, which this port does not have: it must refuse both guests +# (launch refused mask 0x3, no mailbox written). # # The probe is the board's MCU-Link, the one pyOCD lists as # "MCU-LINK FRDM-MCXN947" (MCXN947_PROBE_UID picks one of several); the @@ -41,6 +44,9 @@ repo="$(cd "$here/../.." && pwd)" work="${MCXN947_WORK:-$repo/logs/mcxn947-hw}" lock="${MCXN947_HW_LOCK:-$repo/logs/mcxn947-hw.lock}" engine="${WT_ENGINE:-native}" +case "$scenario" in + wrpoff) export WT_GUEST_FLASH_WRP=1 ;; +esac # Each run's console, readbacks and SWD dump, kept per scenario and engine. evidence="$work/$scenario-$engine" target="${MCXN947_TARGET:-mcxn947vdf}" @@ -157,11 +163,15 @@ clear_mailboxes() { local verified refused verified="$(elf_sym g_wt_launch_verified_mask)" refused="$(elf_sym g_wt_launch_refused_mask)" - [ -n "$verified" ] && [ -n "$refused" ] || fail "launch masks not found in wolftrust.elf" + [ -n "$refused" ] || fail "launch masks not found in wolftrust.elf" + # A WT_GUEST_FLASH_WRP=1 image here verifies no guest, so the linker drops + # the verified mask; every other image must carry it. + [ -n "$verified" ] || [ "${WT_GUEST_FLASH_WRP:-0}" = "1" ] || + fail "launch masks not found in wolftrust.elf" board_cmds+=(-c "write32 $guest0_ram 0 0 0 0 0 0 0 0 0 0 0 0" \ -c "write32 $guest1_ram 0 0 0 0 0 0 0 0 0 0 0 0" \ - -c "write32 $probe_addr 0" \ - -c "write32 $verified 0" -c "write32 $refused 0") + -c "write32 $probe_addr 0" -c "write32 $refused 0") + [ -z "$verified" ] || board_cmds+=(-c "write32 $verified 0") } # The commander exits 0 whatever a command did, so its output is checked too. @@ -477,8 +487,23 @@ psa) check_secure_side log "PASS: hardware/mcxn947/$scenario" ;; +wrpoff) + run_chain "$baremetal_dir" "" + check "$([ -z "$(elf_sym g_wt_launch_verified_mask)" ] || + [ "$(sym_word g_wt_launch_verified_mask)" = "00000000" ]; echo $?)" \ + "no guest was launch-verified (the image has no path that marks one)" + check "$([ "$(sym_word g_wt_launch_refused_mask)" = "00000003" ]; echo $?)" \ + "launch-refused guest mask 0x$(sym_word g_wt_launch_refused_mask) (want 0x00000003)" + for g in "0:$guest0_ram" "1:$guest1_ram"; do + sig="$(word_at "${g##*:}" 0)" + check "$([ "$sig" = "00000000" ]; echo $?)" \ + "guest${g%%:*} never entered its domain (mailbox 0x$sig)" + done + check_secure_side + log "PASS: hardware/mcxn947/$scenario" + ;; *) - log "usage: $0 positive|sauneg|periphneg|restart|psa" + log "usage: $0 positive|sauneg|periphneg|restart|psa|wrpoff" exit 2 ;; esac diff --git a/tests/target/run_mcxn947_suite.sh b/tests/target/run_mcxn947_suite.sh index 1a48a99f..47b7ed43 100755 --- a/tests/target/run_mcxn947_suite.sh +++ b/tests/target/run_mcxn947_suite.sh @@ -13,7 +13,7 @@ set -euo pipefail repo="$(cd "$(dirname "$0")/../.." && pwd)" runner="${MCXN947_SUITE_RUNNER:-$repo/tests/target/run_mcxn947_hardware.sh}" detect="${MCXN947_SUITE_DETECT:-$repo/tests/target/detect_mcxn947.sh}" -scenarios="${WT_MCXN947_SCENARIOS:-positive sauneg periphneg restart psa}" +scenarios="${WT_MCXN947_SCENARIOS:-positive sauneg periphneg restart psa wrpoff}" spsdk_venv="${MCXN947_SPSDK_VENV:-$HOME/spsdk-venv}" logs="${MCXN947_SUITE_LOGS:-$repo/logs/mcxn947-suite}" lock="${MCXN947_HW_LOCK:-$repo/logs/mcxn947-hw.lock}" From a097417ede381d09b9834ce562d27e65ff3da0e3 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 5 Nov 2026 19:30:47 -0800 Subject: [PATCH 45/45] Place the MCXN947 NSPE test buffer in guest0, size the FWU probe payload, and use one pyOCD --- port/mcxn947/conformance/conf_nvm_sync.c | 5 +++++ port/mcxn947/conformance/pal_config.h | 4 ++-- tests/firmware/psa-guest/main.c | 8 +++++++- tests/target/detect_mcxn947.sh | 1 + tests/target/run_mcxn947_hardware.sh | 16 +++++++++------- tests/target/test_mcxn947_suite_evidence.sh | 2 +- 6 files changed, 25 insertions(+), 11 deletions(-) diff --git a/port/mcxn947/conformance/conf_nvm_sync.c b/port/mcxn947/conformance/conf_nvm_sync.c index 4006423e..22b43819 100644 --- a/port/mcxn947/conformance/conf_nvm_sync.c +++ b/port/mcxn947/conformance/conf_nvm_sync.c @@ -47,6 +47,11 @@ WT_CONF_DRV_MMIO_SIZE) #error "pal_config DRIVER MMIO does not match memory_map WT_CONF_DRV_MMIO" #endif +/* i052/i053 need a valid NSPE base so only the range end is invalid. */ +#if NSPE_MMIO_0_START < WT_GUEST0_RAM_BASE || \ + NSPE_MMIO_0_END >= (WT_GUEST0_RAM_BASE + WT_GUEST_RAM_SIZE) +#error "pal_config NSPE MMIO must lie inside guest0's RAM window" +#endif int wt_conf_nvm_sync(uint8_t *buf, uint32_t len, int store) { diff --git a/port/mcxn947/conformance/pal_config.h b/port/mcxn947/conformance/pal_config.h index 38ff66be..b1ee1351 100644 --- a/port/mcxn947/conformance/pal_config.h +++ b/port/mcxn947/conformance/pal_config.h @@ -51,8 +51,8 @@ #define NVMEM_0_PERMISSION TYPE_READ_WRITE #define NSPE_MMIO_NUM 1 -#define NSPE_MMIO_0_START 0x20041F00 -#define NSPE_MMIO_0_END 0x20041F1F +#define NSPE_MMIO_0_START 0x20010F00 +#define NSPE_MMIO_0_END 0x20010F1F #define NSPE_MMIO_0_PERMISSION TYPE_READ_WRITE /* Per-partition pseudo-MMIO holes carved from the top of the shared CONFDATA diff --git a/tests/firmware/psa-guest/main.c b/tests/firmware/psa-guest/main.c index 083f076d..7c7ae8f2 100644 --- a/tests/firmware/psa-guest/main.c +++ b/tests/firmware/psa-guest/main.c @@ -854,6 +854,12 @@ static void exercise_hsm_attack_probe(void) #if WT_FWU_PROBE_TAIL_BYTES < 1 || WT_FWU_PROBE_TAIL_BYTES > 512 #error "WT_FWU_PROBE_TAIL_BYTES must fit the probe's 512-byte write payload" #endif +#if defined(WT_FWU_PROBE_STREAM_BYTES) +#define WT_FWU_PROBE_PAYLOAD_BYTES \ + ((uint32_t)WT_FWU_PROBE_STREAM_BYTES - 1024u + WT_FWU_PROBE_TAIL_BYTES) +#else +#define WT_FWU_PROBE_PAYLOAD_BYTES ((uint32_t)WT_FWU_PROBE_TAIL_BYTES) +#endif #define GUEST_FWU_OP_START 2u #define GUEST_FWU_OP_WRITE 3u #define GUEST_FWU_OP_FINISH 4u @@ -941,7 +947,7 @@ static void exercise_fwu(void) memset(writebuf + 16, 0xFF, 512u); word = 0x464C4F57u; memcpy(writebuf + 16, &word, 4u); - word = 32u; + word = WT_FWU_PROBE_PAYLOAD_BYTES; memcpy(writebuf + 20, &word, 4u); writebuf[24] = 0x01u; writebuf[25] = 0x00u; diff --git a/tests/target/detect_mcxn947.sh b/tests/target/detect_mcxn947.sh index 4e022b0b..6a071d04 100755 --- a/tests/target/detect_mcxn947.sh +++ b/tests/target/detect_mcxn947.sh @@ -11,6 +11,7 @@ MCXN947_SPSDK_VENV="${MCXN947_SPSDK_VENV:-$HOME/spsdk-venv}" if ! command -v "$MCXN947_PYOCD" >/dev/null 2>&1; then if [ -x "$MCXN947_SPSDK_VENV/bin/pyocd" ]; then + MCXN947_PYOCD="$MCXN947_SPSDK_VENV/bin/pyocd" PATH="$MCXN947_SPSDK_VENV/bin:$PATH" export PATH else diff --git a/tests/target/run_mcxn947_hardware.sh b/tests/target/run_mcxn947_hardware.sh index 18ac15f6..440b81d9 100755 --- a/tests/target/run_mcxn947_hardware.sh +++ b/tests/target/run_mcxn947_hardware.sh @@ -54,6 +54,7 @@ probe_uid="${MCXN947_PROBE_UID:-}" console="${MCXN947_CONSOLE:-}" wolfboot_dir="${MCXN947_WOLFBOOT_DIR:-}" spsdk_venv="${MCXN947_SPSDK_VENV:-$HOME/spsdk-venv}" +pyocd="${MCXN947_PYOCD:-pyocd}" console_seconds="${MCXN947_CONSOLE_SECONDS:-25}" # Physical flash offsets (port/mcxn947/memory_map.h, wolfBoot's # mcxn-tz-wolftrust config): the debugger and the ROM see the array at 0. @@ -99,8 +100,9 @@ check() { } ensure_spsdk() { - if ! command -v pyocd >/dev/null 2>&1; then - [ -x "$spsdk_venv/bin/pyocd" ] || fail "pyocd not found (set MCXN947_SPSDK_VENV)" + if ! command -v "$pyocd" >/dev/null 2>&1; then + [ -x "$spsdk_venv/bin/pyocd" ] || fail "pyocd not found (set MCXN947_PYOCD or MCXN947_SPSDK_VENV)" + pyocd="$spsdk_venv/bin/pyocd" PATH="$spsdk_venv/bin:$PATH" export PATH fi @@ -111,7 +113,7 @@ ensure_spsdk() { ensure_probe() { local listed if [ -z "$probe_uid" ]; then - listed="$(pyocd list 2>/dev/null | grep -F "MCU-LINK FRDM-MCXN947" || true)" + listed="$("$pyocd" list 2>/dev/null | grep -F "MCU-LINK FRDM-MCXN947" || true)" [ "$(printf '%s\n' "$listed" | grep -c .)" -eq 1 ] || \ fail "expected one MCU-Link FRDM-MCXN947 probe (set MCXN947_PROBE_UID)" probe_uid="$(printf '%s\n' "$listed" | awk '{ for (i = 1; i <= NF; i++) if ($i ~ /^[A-Z0-9]{8,}$/) { print $i; exit } }')" @@ -129,7 +131,7 @@ ensure_probe() { # the chip on connect, so a read would land in a fresh boot, not the one under # test. dap() { - timeout 60 pyocd cmd -u "$probe_uid" -t cortex_m "$@" 2>&1 | + timeout 60 "$pyocd" cmd -u "$probe_uid" -t cortex_m "$@" 2>&1 | grep -viE "rom table|APB-AP|coresight|cidr" } @@ -178,7 +180,7 @@ clear_mailboxes() { program_board() { local out rc=0 [ "${#board_cmds[@]}" -gt 0 ] || fail "nothing to program" - out="$(timeout 900 pyocd cmd -u "$probe_uid" -t "$target" -O resume_on_disconnect=false \ + out="$(timeout 900 "$pyocd" cmd -u "$probe_uid" -t "$target" -O resume_on_disconnect=false \ "${park_cmds[@]}" "${board_cmds[@]}" 2>&1)" || rc=$? if [ "$rc" -ne 0 ]; then printf '%s\n' "$out" | tail -6 @@ -203,7 +205,7 @@ release_core() { stty -F "$console" 115200 raw -echo -crtscts -ixon 2>/dev/null || true ( timeout "$console_seconds" cat "$console" > "$evidence/console.log" 2>/dev/null ) & sleep 0.5 - timeout 60 pyocd cmd -u "$probe_uid" -t "$target" -O resume_on_disconnect=false \ + timeout 60 "$pyocd" cmd -u "$probe_uid" -t "$target" -O resume_on_disconnect=false \ -O reset_type=hw -c "reset halt" -c "write32 0xE0001028 0" \ -c "write32 0xE0001038 0" -c "write32 0xE000ED30 0x1F" -c "go" \ >/dev/null 2>&1 || fail "could not release the core" @@ -220,7 +222,7 @@ verify_at() { local addr="$1" image="$2" size back size="$(wc -c < "$image" | tr -d ' ')" back="$evidence/readback-$(basename "$image")" - timeout 120 pyocd cmd -u "$probe_uid" -t cortex_m \ + timeout 120 "$pyocd" cmd -u "$probe_uid" -t cortex_m \ -c "savemem $addr $size $back" >/dev/null 2>&1 || \ fail "readback of $addr failed" cmp -s "$back" "$image" || \ diff --git a/tests/target/test_mcxn947_suite_evidence.sh b/tests/target/test_mcxn947_suite_evidence.sh index 8803b451..eb884117 100755 --- a/tests/target/test_mcxn947_suite_evidence.sh +++ b/tests/target/test_mcxn947_suite_evidence.sh @@ -78,7 +78,7 @@ pyocd() { case "$PB_MODE" in silent) return 124 ;; error) echo "Error: transfer failed"; return 0 ;; longerror) echo "Error: transfer failed"; printf "%02000000d\n" 0; return 0 ;; *) echo "loaded"; return 0 ;; esac; } -probe_uid=X; target=mcxn947vdf +probe_uid=X; target=mcxn947vdf; pyocd=pyocd park_cmds=(-c "reset halt"); board_cmds=(-c "load a.bin 0x0") . "$1" program_board && echo RELEASED' _ "$tmp/program_board.sh" 2>&1