diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml index 0bab547865..08d998b657 100644 --- a/.github/actionlint.yaml +++ b/.github/actionlint.yaml @@ -12,3 +12,4 @@ self-hosted-runner: - pqcp-x64 # RISE RISC-V runner - ubuntu-24.04-riscv + - self-hosted-nucleo-n657x0 diff --git a/.github/actions/bench/action.yml b/.github/actions/bench/action.yml index 93148afc20..54ceb7b0d9 100644 --- a/.github/actions/bench/action.yml +++ b/.github/actions/bench/action.yml @@ -89,10 +89,6 @@ runs: --ldflags="${{ inputs.ldflags }}" \ --opt=$([[ ${{ inputs.opt }} == "false" ]] && echo "no_opt" || echo "opt") \ -v ${{ inputs.bench_extra_args }} - - name: Check namespace - shell: ${{ env.SHELL }} - run: | - check-namespace - name: Store benchmark result uses: benchmark-action/github-action-benchmark@52576c92bccf6ac60c8223ec7eb2565637cae9ba # v1.22.1 with: diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 3ba9d960f7..06da220a87 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -38,6 +38,8 @@ jobs: bench_extra_args: "" nix_shell: bench only_no_opt: false + extra_makefile: "" + zephyr_target: "" - system: rpi5 name: Arm Cortex-A76 (Raspberry Pi 5) benchmarks bench_pmu: PERF @@ -48,6 +50,8 @@ jobs: nix_shell: bench only_no_opt: false cross_prefix: "" + extra_makefile: "" + zephyr_target: "" - system: a55 name: Arm Cortex-A55 (Snapdragon 888) benchmarks bench_pmu: PERF @@ -57,6 +61,8 @@ jobs: bench_extra_args: -w exec-on-a55 nix_shell: bench only_no_opt: false + extra_makefile: "" + zephyr_target: "" - system: bpi name: SpacemiT K1 8 (Banana Pi F3) benchmarks bench_pmu: PERF @@ -67,6 +73,8 @@ jobs: cross_prefix: riscv64-unknown-linux-gnu- nix_shell: cross-riscv64 only_no_opt: true + extra_makefile: "" + zephyr_target: "" - system: m1-mac-mini name: Mac Mini (M1, 2020) benchmarks bench_pmu: MAC @@ -76,12 +84,29 @@ jobs: bench_extra_args: "-r" nix_shell: bench only_no_opt: false + extra_makefile: "" + zephyr_target: "" + - system: nucleo-n657x0 + name: Arm Cortex-M55 (NUCLEO-N657X0-Q) benchmarks + bench_pmu: NO + archflags: "" + cflags: "" + ldflags: "" + bench_extra_args: "" + nix_shell: zephyr + only_no_opt: false + cross_prefix: "" + extra_makefile: test/zephyr/platform.mk + zephyr_target: nucleo-n657x0-q if: github.repository_owner == 'pq-code-package' && !github.event.pull_request.head.repo.fork && (github.event.label.name == 'benchmark' || github.ref == 'refs/heads/main') runs-on: self-hosted-${{ matrix.target.system }} steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: ./.github/actions/bench if: ${{ !matrix.target.only_no_opt }} + env: + EXTRA_MAKEFILE: ${{ matrix.target.extra_makefile }} + ZEPHYR_TARGET: ${{ matrix.target.zephyr_target }} with: name: ${{ matrix.target.name }} (opt) cflags: ${{ matrix.target.cflags }} @@ -95,6 +120,9 @@ jobs: cross_prefix: ${{ matrix.target.cross_prefix }} opt: true - uses: ./.github/actions/bench + env: + EXTRA_MAKEFILE: ${{ matrix.target.extra_makefile }} + ZEPHYR_TARGET: ${{ matrix.target.zephyr_target }} with: name: ${{ matrix.target.name }} (no-opt) cflags: ${{ matrix.target.cflags }} diff --git a/.github/workflows/zephyr.yml b/.github/workflows/zephyr.yml index 3b01d3f69b..6341734653 100644 --- a/.github/workflows/zephyr.yml +++ b/.github/workflows/zephyr.yml @@ -53,5 +53,5 @@ jobs: ZEPHYR_TARGET: ${{ matrix.target.board }} run: | opt=${{ matrix.target.opt == 'all' && 'opt' || 'no_opt' }} - nix develop .#zephyr --command ./scripts/tests bench --no-auto -c PMU --opt=$opt - nix develop .#zephyr --command ./scripts/tests bench --no-auto --components -c PMU --opt=$opt + nix develop .#zephyr --command ./scripts/tests bench --no-auto -c NO --opt=$opt + nix develop .#zephyr --command ./scripts/tests bench --no-auto --components -c NO --opt=$opt diff --git a/BIBLIOGRAPHY.md b/BIBLIOGRAPHY.md index f9bf45bf75..22fad9971c 100644 --- a/BIBLIOGRAPHY.md +++ b/BIBLIOGRAPHY.md @@ -88,6 +88,7 @@ source code and documentation. - [mldsa/mldsa_native_config.h](mldsa/mldsa_native_config.h) - [mldsa/src/sign.c](mldsa/src/sign.c) - [proofs/cbmc/mldsa_native_config_cbmc.h](proofs/cbmc/mldsa_native_config_cbmc.h) + - [test/configs/armv81m_mve_memory_config.h](test/configs/armv81m_mve_memory_config.h) - [test/configs/break_pct_config.h](test/configs/break_pct_config.h) - [test/configs/custom_heap_alloc_config.h](test/configs/custom_heap_alloc_config.h) - [test/configs/custom_memcpy_config.h](test/configs/custom_memcpy_config.h) @@ -151,6 +152,7 @@ source code and documentation. - [mldsa/src/sign.c](mldsa/src/sign.c) - [mldsa/src/sign.h](mldsa/src/sign.h) - [proofs/cbmc/mldsa_native_config_cbmc.h](proofs/cbmc/mldsa_native_config_cbmc.h) + - [test/configs/armv81m_mve_memory_config.h](test/configs/armv81m_mve_memory_config.h) - [test/configs/break_pct_config.h](test/configs/break_pct_config.h) - [test/configs/custom_heap_alloc_config.h](test/configs/custom_heap_alloc_config.h) - [test/configs/custom_memcpy_config.h](test/configs/custom_memcpy_config.h) diff --git a/STDLIB.md b/STDLIB.md index 4a7830f38f..d45d59ac33 100644 --- a/STDLIB.md +++ b/STDLIB.md @@ -24,4 +24,6 @@ Replaces all `memcpy` calls with a custom implementation. When enabled, you must ### MLD_CONFIG_CUSTOM_MEMSET Replaces all `memset` calls with a custom implementation. When enabled, you must define a `mld_memset` function with the same signature as the standard `memset`. -See the configuration examples in `mldsa/src/config.h` and test configurations in `test/custom_*_config.h` for usage examples and implementation requirements. +Armv8.1-M MVE targets can use `mldsa/src/native/armv81m/mve_memory.h` as an implementation of the existing `MLD_CONFIG_CUSTOM_ZEROIZE`, `MLD_CONFIG_CUSTOM_MEMCPY`, and `MLD_CONFIG_CUSTOM_MEMSET` hooks. + +See the configuration examples in `mldsa/mldsa_native_config.h` and test configurations in `test/configs/*_config.h` for usage examples and implementation requirements. diff --git a/flake.nix b/flake.nix index e68ec05b3f..5fe88aae34 100644 --- a/flake.nix +++ b/flake.nix @@ -169,9 +169,12 @@ # Zephyr build environment (board chosen at make time via EXTRA_MAKEFILE) packages.zephyr = util.zephyr; + # OpenOCD with STM32N6 support, for the NUCLEO-N657X0-Q hardware target. + packages.openocd = util.openocd; devShells.zephyr = util.mkShell { packages = builtins.attrValues { + inherit (config.packages) openocd; inherit (util) zephyr; inherit (pkgs) gcc-arm-embedded qemu cmake ninja dtc gperf coreutils git; } ++ [ util.zephyrPythonEnv ]; diff --git a/mldsa/mldsa_native.c b/mldsa/mldsa_native.c index 9ce66dcf27..c983b6f81b 100644 --- a/mldsa/mldsa_native.c +++ b/mldsa/mldsa_native.c @@ -667,6 +667,11 @@ #undef MLD_NATIVE_FUNC_SUCCESS #undef MLD_NTT_BOUND #undef MLD_REDUCE32_RANGE_MAX +/* mldsa/src/native/armv81m/mve_memory.h */ +#undef MLD_NATIVE_ARMV81M_MVE_MEMORY_H +#undef mld_memcpy +#undef mld_memset +#undef mld_zeroize /* mldsa/src/native/meta.h */ #undef MLD_NATIVE_META_H #if defined(MLD_SYS_AARCH64) diff --git a/mldsa/mldsa_native_asm.S b/mldsa/mldsa_native_asm.S index 8a130e04c0..3acc38d133 100644 --- a/mldsa/mldsa_native_asm.S +++ b/mldsa/mldsa_native_asm.S @@ -688,6 +688,11 @@ #undef MLD_NATIVE_FUNC_SUCCESS #undef MLD_NTT_BOUND #undef MLD_REDUCE32_RANGE_MAX +/* mldsa/src/native/armv81m/mve_memory.h */ +#undef MLD_NATIVE_ARMV81M_MVE_MEMORY_H +#undef mld_memcpy +#undef mld_memset +#undef mld_zeroize /* mldsa/src/native/meta.h */ #undef MLD_NATIVE_META_H #if defined(MLD_SYS_AARCH64) diff --git a/mldsa/src/native/armv81m/mve_memory.h b/mldsa/src/native/armv81m/mve_memory.h new file mode 100644 index 0000000000..216529770e --- /dev/null +++ b/mldsa/src/native/armv81m/mve_memory.h @@ -0,0 +1,87 @@ +/* + * Copyright (c) The mldsa-native project authors + * SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + */ + +#ifndef MLD_NATIVE_ARMV81M_MVE_MEMORY_H +#define MLD_NATIVE_ARMV81M_MVE_MEMORY_H + +#include "../../sys.h" + +#if !defined(__ASSEMBLER__) + +#if !defined(MLD_SYS_ARMV81M_MVE) +#error "Armv8.1-M MVE memory helpers require Armv8.1-M MVE" +#endif + +#include + +static MLD_INLINE void mld_zeroize_native(void *ptr, size_t len) +{ + __asm__ __volatile__( + " vdup.8 q0, %[set_val] \n" + " wlstp.8 lr, %[cnt], 1f \n" + "2: \n" + " vstrb.8 q0, [%[out]], #16 \n" + " letp lr, 2b \n" + "1: \n" + : [out] "+r"(ptr) + : [cnt] "r"(len), [set_val] "r"(0) + : "q0", "memory", "r14"); +} + +static MLD_INLINE void *mld_memset_native(void *dest, int c, size_t n) +{ + void *ret = dest; + + __asm__ __volatile__( + " vdup.8 q0, %[set_val] \n" + " wlstp.8 lr, %[cnt], 1f \n" + "2: \n" + " vstrb.8 q0, [%[out]], #16 \n" + " letp lr, 2b \n" + "1: \n" + : [out] "+r"(dest) + : [cnt] "r"(n), [set_val] "r"(c) + : "q0", "memory", "r14"); + + return ret; +} + +static MLD_INLINE void *mld_memcpy_native(void *dest, const void *src, size_t n) +{ + void *ret = dest; + + __asm__ __volatile__( + " wlstp.8 lr, %[cnt], 1f \n" + "2: \n" + " vldrb.8 q0, [%[in]], #16 \n" + " vstrb.8 q0, [%[out]], #16 \n" + " letp lr, 2b \n" + "1: \n" + : [in] "+r"(src), [out] "+r"(dest) + : [cnt] "r"(n) + : "q0", "memory", "r14"); + + return ret; +} + +#endif /* !__ASSEMBLER__ */ + +#endif /* !MLD_NATIVE_ARMV81M_MVE_MEMORY_H */ + +#if !defined(__ASSEMBLER__) + +#if defined(MLD_CONFIG_CUSTOM_ZEROIZE) +#define mld_zeroize mld_zeroize_native +#endif + +#if defined(MLD_CONFIG_CUSTOM_MEMSET) +#define mld_memset mld_memset_native +#endif + +#if defined(MLD_CONFIG_CUSTOM_MEMCPY) +#define mld_memcpy mld_memcpy_native +#endif + +#endif /* !__ASSEMBLER__ */ diff --git a/nix/openocd/default.nix b/nix/openocd/default.nix new file mode 100644 index 0000000000..3c68ebe73a --- /dev/null +++ b/nix/openocd/default.nix @@ -0,0 +1,22 @@ +# Copyright (c) The mldsa-native project authors +# Copyright (c) The mlkem-native project authors +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +{ fetchFromGitHub +, openocd +, autoreconfHook +}: + +openocd.overrideAttrs (old: rec { + pname = "openocd"; + version = "unstable-2026-05-01"; + nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ autoreconfHook ]; + + src = fetchFromGitHub { + owner = "openocd-org"; + repo = "openocd"; + rev = "4e9b167e1ae5ccb437eb0538440988b3f0ec53cb"; + fetchSubmodules = true; + hash = "sha256-8aYl7JzulPxH6vgSeTKTMIZVH6d55JJlXTBkfgAPTbU="; + }; +}) diff --git a/nix/util.nix b/nix/util.nix index 8465c445cb..aced6ad76c 100644 --- a/nix/util.nix +++ b/nix/util.nix @@ -109,6 +109,7 @@ rec { hol_server = pkgs.callPackage ./hol_light/hol_server.nix { inherit hol_light'; }; s2n_bignum = pkgs.callPackage ./s2n_bignum { }; slothy = pkgs.callPackage ./slothy { }; + openocd = pkgs.callPackage ./openocd { }; zephyr = pkgs.callPackage ./zephyr { }; zephyrPythonEnv = pkgs.python3.withPackages (ps: with ps; [ pyelftools diff --git a/nix/zephyr/default.nix b/nix/zephyr/default.nix index e80b3395f7..94ebbd22a8 100644 --- a/nix/zephyr/default.nix +++ b/nix/zephyr/default.nix @@ -10,8 +10,7 @@ # Board-agnostic Zephyr build environment: a pinned Zephyr tree plus the # modules needed by the boards we target, exposed via a setup hook so a plain -# `cmake` build works with no west workspace. CMSIS-6 covers the Cortex-M -# boards; add further modules here as more boards are wired up. +# `cmake` build works with no west workspace. let zephyr = fetchFromGitHub { owner = "zephyrproject-rtos"; @@ -27,6 +26,14 @@ let rev = "30a859f44ef8ab4dc8f84b03ed586fd16ccf9d74"; hash = "sha256-nTehISN0pu9gnOZMpGaBQ3DFmNxAqAZPGpvbKfEM35o="; }; + + # Revision pinned by the Zephyr v4.4.1 manifest (west.yml). + hal_stm32 = fetchFromGitHub { + owner = "zephyrproject-rtos"; + repo = "hal_stm32"; + rev = "fc11896dd39cfca37bf9b4aeaaa2df8861b81875"; + hash = "sha256-AtNq2yTZsTFMTlWn/Ns0wuEiN4Wv/OTV2vWPRu0SnOE="; + }; in stdenvNoCC.mkDerivation { pname = "mldsa-native-zephyr"; @@ -38,11 +45,14 @@ stdenvNoCC.mkDerivation { mkdir -p $out ln -s ${zephyr} $out/zephyr ln -s ${cmsis_6} $out/cmsis_6 + ln -s ${hal_stm32} $out/hal_stm32 ''; setupHook = writeText "setup-hook.sh" '' export ZEPHYR_BASE="$1/zephyr" - export ZEPHYR_MODULES="$1/cmsis_6" + export ZEPHYR_CMSIS_6_MODULE="$1/cmsis_6" + export ZEPHYR_HAL_STM32_MODULE="$1/hal_stm32" + export ZEPHYR_MODULES="$1/cmsis_6;$1/hal_stm32" export ZEPHYR_TOOLCHAIN_VARIANT=gnuarmemb export GNUARMEMB_TOOLCHAIN_PATH=${gcc-arm-embedded} ''; diff --git a/scripts/tests b/scripts/tests index dbb063e0fd..cb56ea3b55 100755 --- a/scripts/tests +++ b/scripts/tests @@ -782,8 +782,11 @@ class Tests: test_type, self.do_opt(), suppress_output=False ) - if resultss is None: + # Nothing below should write benchmark output unless the run produced + # results and all compile/run steps succeeded. + if resultss is None or len(self.failed) > 0: self.check_fail() + return # NOTE: There will only be one items in resultss, as we haven't yet decided how to write both opt/no-opt benchmark results for k, results in resultss.items(): diff --git a/test/bench/bench_mldsa.c b/test/bench/bench_mldsa.c index a8b5a3851d..15d11f2014 100644 --- a/test/bench/bench_mldsa.c +++ b/test/bench/bench_mldsa.c @@ -42,7 +42,10 @@ static int cmp_uint64_t(const void *a, const void *b) { - return (int)((*((const uint64_t *)a)) - (*((const uint64_t *)b))); + const uint64_t va = *((const uint64_t *)a); + const uint64_t vb = *((const uint64_t *)b); + + return (va > vb) - (va < vb); } static void print_avg(const char *txt, uint64_t cyc[MLD_BENCHMARK_NTESTS]) @@ -66,7 +69,7 @@ static void print_percentile_legend(void) printf("%21s", "percentile"); for (i = 0; i < sizeof(percentiles) / sizeof(percentiles[0]); i++) { - printf("%9d", percentiles[i]); + printf("%12d", percentiles[i]); } printf("\n"); } @@ -78,8 +81,8 @@ static void print_percentiles(const char *txt, printf("%10s percentiles:", txt); for (i = 0; i < sizeof(percentiles) / sizeof(percentiles[0]); i++) { - printf("%9" PRIu64, (cyc)[MLD_BENCHMARK_NTESTS * percentiles[i] / 100] / - MLD_BENCHMARK_NITERATIONS); + printf("%12" PRIu64, (cyc)[MLD_BENCHMARK_NTESTS * percentiles[i] / 100] / + MLD_BENCHMARK_NITERATIONS); } printf("\n"); } diff --git a/test/configs/armv81m_mve_memory_config.h b/test/configs/armv81m_mve_memory_config.h new file mode 100644 index 0000000000..c1b25b4699 --- /dev/null +++ b/test/configs/armv81m_mve_memory_config.h @@ -0,0 +1,746 @@ +/* + * Copyright (c) The mldsa-native project authors + * SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + */ + +/* References + * ========== + * + * - [FIPS140_3_IG] + * Implementation Guidance for FIPS 140-3 and the Cryptographic Module + * Validation Program + * National Institute of Standards and Technology + * https://csrc.nist.gov/projects/cryptographic-module-validation-program/fips-140-3-ig-announcements + * + * - [FIPS204] + * FIPS 204 Module-Lattice-Based Digital Signature Standard + * National Institute of Standards and Technology + * https://csrc.nist.gov/pubs/fips/204/final + */ + +/* + * WARNING: This file is auto-generated from scripts/autogen + * in the mldsa-native repository. + * Do not modify it directly. + */ + +/* + * Test configuration: Test configuration with Armv8.1-M MVE custom zeroize, + * memcpy, and memset + * + * This configuration differs from the default mldsa/mldsa_native_config.h in + * the following places: + * - MLD_CONFIG_CUSTOM_ZEROIZE + * - MLD_CONFIG_CUSTOM_MEMCPY + * - MLD_CONFIG_CUSTOM_MEMSET + */ + + +#ifndef MLD_CONFIG_H +#define MLD_CONFIG_H + +/** + * MLD_CONFIG_PARAMETER_SET + * + * Specifies the parameter set for ML-DSA + * - MLD_CONFIG_PARAMETER_SET=44 corresponds to ML-DSA-44 + * - MLD_CONFIG_PARAMETER_SET=65 corresponds to ML-DSA-65 + * - MLD_CONFIG_PARAMETER_SET=87 corresponds to ML-DSA-87 + * + * If you want to support multiple parameter sets, build the + * library multiple times and set MLD_CONFIG_MULTILEVEL_BUILD. + * See MLD_CONFIG_MULTILEVEL_BUILD for how to do this while + * minimizing code duplication. + * + * This can also be set using CFLAGS. + */ +#ifndef MLD_CONFIG_PARAMETER_SET +#define MLD_CONFIG_PARAMETER_SET \ + 44 /* Change this for different security strengths */ +#endif + +/** + * MLD_CONFIG_FILE + * + * If defined, this is a header that will be included instead + * of the default configuration file mldsa/mldsa_native_config.h. + * + * When you need to build mldsa-native in multiple configurations, + * using varying MLD_CONFIG_FILE can be more convenient + * then configuring everything through CFLAGS. + * + * To use, MLD_CONFIG_FILE _must_ be defined prior + * to the inclusion of any mldsa-native headers. For example, + * it can be set by passing `-DMLD_CONFIG_FILE="..."` + * on the command line. + */ +/* #define MLD_CONFIG_FILE "mldsa_native_config.h" */ + +/** + * MLD_CONFIG_NAMESPACE_PREFIX + * + * The prefix to use to namespace global symbols from mldsa/. + * + * In a multi-level build, level-dependent symbols will + * additionally be prefixed with the parameter set (44/65/87). + * + * This can also be set using CFLAGS. + */ +#if !defined(MLD_CONFIG_NAMESPACE_PREFIX) +#define MLD_CONFIG_NAMESPACE_PREFIX MLD_DEFAULT_NAMESPACE_PREFIX +#endif + +/** + * MLD_CONFIG_MULTILEVEL_BUILD + * + * Set this if the build is part of a multi-level build supporting + * multiple parameter sets. + * + * If you need only a single parameter set, keep this unset. + * + * To build mldsa-native with support for all parameter sets, + * build it three times -- once per parameter set -- and set the + * option MLD_CONFIG_MULTILEVEL_WITH_SHARED for exactly one of + * them, and MLD_CONFIG_MULTILEVEL_NO_SHARED for the others. + * MLD_CONFIG_MULTILEVEL_BUILD should be set for all of them. + * + * See examples/multilevel_build for an example. + * + * This can also be set using CFLAGS. + */ +/* #define MLD_CONFIG_MULTILEVEL_BUILD */ + +/** + * MLD_CONFIG_EXTERNAL_API_QUALIFIER + * + * If set, this option provides an additional function + * qualifier to be added to declarations of mldsa-native's + * public API. + * + * The primary use case for this option are single-CU builds + * where the public API exposed by mldsa-native is wrapped by + * another API in the consuming application. In this case, + * even mldsa-native's public API can be marked `static`. + */ +/* #define MLD_CONFIG_EXTERNAL_API_QUALIFIER */ + +/** + * MLD_CONFIG_NO_KEYPAIR_API + * + * By default, mldsa-native includes support for generating key + * pairs. If you don't need this, set MLD_CONFIG_NO_KEYPAIR_API + * to exclude keypair, keypair_internal, + * pk_from_sk, and all internal APIs only needed by + * those functions. + */ +/* #define MLD_CONFIG_NO_KEYPAIR_API */ + +/** + * MLD_CONFIG_NO_SIGN_API + * + * By default, mldsa-native includes support for creating + * signatures. If you don't need this, set MLD_CONFIG_NO_SIGN_API + * to exclude signature, + * signature_extmu, signature_internal, + * signature_pre_hash_internal, + * signature_pre_hash_shake256, and all internal APIs + * only needed by those functions. + */ +/* #define MLD_CONFIG_NO_SIGN_API */ + +/** + * MLD_CONFIG_NO_VERIFY_API + * + * By default, mldsa-native includes support for verifying + * signatures. If you don't need this, set + * MLD_CONFIG_NO_VERIFY_API to exclude verify, + * verify_extmu, verify_internal, + * verify_pre_hash_internal, + * verify_pre_hash_shake256, and all internal APIs + * only needed by those functions. + */ +/* #define MLD_CONFIG_NO_VERIFY_API */ + +/** + * MLD_CONFIG_CORE_API_ONLY + * + * Set this to remove all public APIs except + * keypair_internal, signature_internal, + * and verify_internal. + */ +/* #define MLD_CONFIG_CORE_API_ONLY */ + +/** + * MLD_CONFIG_NO_RANDOMIZED_API + * + * If this option is set, mldsa-native will be built without the + * randomized API functions (keypair, + * signature, and signature_extmu). + * This allows users to build mldsa-native without providing a + * randombytes() implementation if they only need the + * internal deterministic API + * (keypair_internal, signature_internal). + * + * @note This option is incompatible with MLD_CONFIG_KEYGEN_PCT + * as the current PCT implementation requires + * signature(). + */ +/* #define MLD_CONFIG_NO_RANDOMIZED_API */ + +/** + * MLD_CONFIG_CONSTANTS_ONLY + * + * If you only need the size constants (MLDSA_PUBLICKEYBYTES, etc.) + * but no function declarations, set MLD_CONFIG_CONSTANTS_ONLY. + * + * This only affects the public header mldsa_native.h, not + * the implementation. + */ +/* #define MLD_CONFIG_CONSTANTS_ONLY */ +/****************************************************************************** + * + * Build-only configuration options + * + * The remaining configurations are build-options only. + * They do not affect the API described in mldsa_native.h. + * + *****************************************************************************/ +#if defined(MLD_BUILD_INTERNAL) + +/** + * MLD_CONFIG_MULTILEVEL_WITH_SHARED + * + * This is for multi-level builds of mldsa-native only. If you + * need only a single parameter set, keep this unset. + * + * If this is set, all MLD_CONFIG_PARAMETER_SET-independent + * code will be included in the build, including code needed only + * for other parameter sets. + * + * Example: TODO: add example + * + * To build mldsa-native with support for all parameter sets, + * build it three times -- once per parameter set -- and set the + * option MLD_CONFIG_MULTILEVEL_WITH_SHARED for exactly one of + * them, and MLD_CONFIG_MULTILEVEL_NO_SHARED for the others. + * + * See examples/multilevel_build_mldsa for an example. + * + * This can also be set using CFLAGS. + */ +/* #define MLD_CONFIG_MULTILEVEL_WITH_SHARED */ + +/** + * MLD_CONFIG_MULTILEVEL_NO_SHARED + * + * This is for multi-level builds of mldsa-native only. If you + * need only a single parameter set, keep this unset. + * + * If this is set, no MLD_CONFIG_PARAMETER_SET-independent code + * will be included in the build. + * + * To build mldsa-native with support for all parameter sets, + * build it three times -- once per parameter set -- and set the + * option MLD_CONFIG_MULTILEVEL_WITH_SHARED for exactly one of + * them, and MLD_CONFIG_MULTILEVEL_NO_SHARED for the others. + * + * See examples/multilevel_build_mldsa for an example. + * + * This can also be set using CFLAGS. + */ +/* #define MLD_CONFIG_MULTILEVEL_NO_SHARED */ + +/** + * MLD_CONFIG_MONOBUILD_KEEP_SHARED_HEADERS + * + * This is only relevant for single compilation unit (SCU) + * builds of mldsa-native. In this case, it determines whether + * directives defined in parameter-set-independent headers should + * be #undef'ined or not at the of the SCU file. This is needed + * in multilevel builds. + * + * See examples/multilevel_build_native for an example. + * + * This can also be set using CFLAGS. + */ +/* #define MLD_CONFIG_MONOBUILD_KEEP_SHARED_HEADERS */ + +/** + * MLD_CONFIG_USE_NATIVE_BACKEND_ARITH + * + * Determines whether an native arithmetic backend should be used. + * + * The arithmetic backend covers performance-critical functions + * such as the number-theoretic transform (NTT). + * + * If this option is unset, the C backend will be used. + * + * If this option is set, the arithmetic backend to be use is + * determined by MLD_CONFIG_ARITH_BACKEND_FILE: If the latter is + * unset, the default backend for your the target architecture + * will be used. If set, it must be the name of a backend metadata + * file. + * + * This can also be set using CFLAGS. + */ +#if !defined(MLD_CONFIG_USE_NATIVE_BACKEND_ARITH) +/* #define MLD_CONFIG_USE_NATIVE_BACKEND_ARITH */ +#endif + +/** + * MLD_CONFIG_ARITH_BACKEND_FILE + * + * The arithmetic backend to use. + * + * If MLD_CONFIG_USE_NATIVE_BACKEND_ARITH is unset, this option + * is ignored. + * + * If MLD_CONFIG_USE_NATIVE_BACKEND_ARITH is set, this option must + * either be undefined or the filename of an arithmetic backend. + * If unset, the default backend will be used. + * + * This can be set using CFLAGS. + */ +#if defined(MLD_CONFIG_USE_NATIVE_BACKEND_ARITH) && \ + !defined(MLD_CONFIG_ARITH_BACKEND_FILE) +#define MLD_CONFIG_ARITH_BACKEND_FILE "native/meta.h" +#endif + +/** + * MLD_CONFIG_USE_NATIVE_BACKEND_FIPS202 + * + * Determines whether an native FIPS202 backend should be used. + * + * The FIPS202 backend covers 1x/2x/4x-fold Keccak-f1600, which is + * the performance bottleneck of SHA3 and SHAKE. + * + * If this option is unset, the C backend will be used. + * + * If this option is set, the FIPS202 backend to be use is + * determined by MLD_CONFIG_FIPS202_BACKEND_FILE: If the latter is + * unset, the default backend for your the target architecture + * will be used. If set, it must be the name of a backend metadata + * file. + * + * This can also be set using CFLAGS. + */ +#if !defined(MLD_CONFIG_USE_NATIVE_BACKEND_FIPS202) +/* #define MLD_CONFIG_USE_NATIVE_BACKEND_FIPS202 */ +#endif + +/** + * MLD_CONFIG_FIPS202_BACKEND_FILE + * + * The FIPS-202 backend to use. + * + * If MLD_CONFIG_USE_NATIVE_BACKEND_FIPS202 is set, this option + * must either be undefined or the filename of a FIPS202 backend. + * If unset, the default backend will be used. + * + * This can be set using CFLAGS. + */ +#if defined(MLD_CONFIG_USE_NATIVE_BACKEND_FIPS202) && \ + !defined(MLD_CONFIG_FIPS202_BACKEND_FILE) +#define MLD_CONFIG_FIPS202_BACKEND_FILE "fips202/native/auto.h" +#endif + +/** + * MLD_CONFIG_FIPS202_CUSTOM_HEADER + * + * Custom header to use for FIPS-202 + * + * This should only be set if you intend to use a custom + * FIPS-202 implementation, different from the one shipped + * with mldsa-native. + * + * If set, it must be the name of a file serving as the + * replacement for mldsa/src/fips202/fips202.h, and exposing + * the same API (see FIPS202.md). + */ +/* #define MLD_CONFIG_FIPS202_CUSTOM_HEADER "SOME_FILE.h" */ + +/** + * MLD_CONFIG_FIPS202X4_CUSTOM_HEADER + * + * Custom header to use for FIPS-202-X4 + * + * This should only be set if you intend to use a custom + * FIPS-202 implementation, different from the one shipped + * with mldsa-native. + * + * If set, it must be the name of a file serving as the + * replacement for mldsa/src/fips202/fips202x4.h, and exposing + * the same API (see FIPS202.md). + */ +/* #define MLD_CONFIG_FIPS202X4_CUSTOM_HEADER "SOME_FILE.h" */ + +/** + * MLD_CONFIG_CUSTOM_ZEROIZE + * + * In compliance with @[FIPS204, Section 3.6.3], mldsa-native, + * zeroizes intermediate stack buffers before returning from + * function calls. + * + * Set this option and define `mld_zeroize` if you want to + * use a custom method to zeroize intermediate stack buffers. + * The default implementation uses SecureZeroMemory on Windows + * and a memset + compiler barrier otherwise. If neither of those + * is available on the target platform, compilation will fail, + * and you will need to use MLD_CONFIG_CUSTOM_ZEROIZE to provide + * a custom implementation of `mld_zeroize()`. + * + * @warning The explicit stack zeroization conducted by mldsa-native reduces + * the likelihood of data leaking on the stack, but does not + * eliminate it. The C standard makes no guarantee about where a + * compiler allocates structures and whether/where it makes copies + * of them. Also, in addition to entire structures, there may also + * be potentially exploitable leakage of individual values on the + * stack. + * + * If you need bullet-proof zeroization of the stack, you need to + * consider additional measures instead of what this feature + * provides. In this case, you can set mld_zeroize to a no-op. + */ +#define MLD_CONFIG_CUSTOM_ZEROIZE +#if !defined(__ASSEMBLER__) +#include "../mldsa/src/native/armv81m/mve_memory.h" +#endif + + +/** + * MLD_CONFIG_CUSTOM_RANDOMBYTES + * + * mldsa-native does not provide a secure randombytes + * implementation. Such an implementation has to provided by the + * consumer. + * + * If this option is not set, mldsa-native expects a function + * int randombytes(uint8_t *out, size_t outlen). + * + * Set this option and define `mld_randombytes` if you want to + * use a custom method to sample randombytes with a different name + * or signature. + */ +/* #define MLD_CONFIG_CUSTOM_RANDOMBYTES + #if !defined(__ASSEMBLER__) + #include + #include "src/src.h" + static MLD_INLINE int mld_randombytes(uint8_t *ptr, size_t len) + { + ... your implementation ... + return 0; + } + #endif +*/ + +/** + * MLD_CONFIG_CUSTOM_CAPABILITY_FUNC + * + * mldsa-native backends may rely on specific hardware features. + * Those backends will only be included in an mldsa-native build + * if support for the respective features is enabled at + * compile-time. However, when building for a heteroneous set + * of CPUs to run the resulting binary/library on, feature + * detection at _runtime_ is needed to decided whether a backend + * can be used or not. + * + * Set this option and define `mld_sys_check_capability` if you + * want to use a custom method to dispatch between implementations. + * + * Return value 1 indicates that a capability is supported. + * Return value 0 indicates that a capability is not supported. + * + * If this option is not set, mldsa-native uses compile-time + * feature detection only to decide which backend to use. + * + * If you compile mldsa-native on a system with different + * capabilities than the system that the resulting binary/library + * will be run on, you must use this option. + */ +/* #define MLD_CONFIG_CUSTOM_CAPABILITY_FUNC + static MLD_INLINE int mld_sys_check_capability(mld_sys_cap cap) + { + ... your implementation ... + } +*/ + +/** + * MLD_CONFIG_CUSTOM_ALLOC_FREE + * + * Set this option and define `MLD_CUSTOM_ALLOC` and + * `MLD_CUSTOM_FREE` if you want to use custom allocation for + * large local structures or buffers. + * + * By default, all buffers/structures are allocated on the stack. + * If this option is set, most of them will be allocated via + * MLD_CUSTOM_ALLOC. + * + * Parameters to MLD_CUSTOM_ALLOC: + * - T* v: Target pointer to declare. + * - T: Type of structure to be allocated + * - N: Number of elements to be allocated. + * + * Parameters to MLD_CUSTOM_FREE: + * - T* v: Target pointer to free. May be NULL. + * - T: Type of structure to be freed. + * - N: Number of elements to be freed. + * + * @warning This option is experimental. Its scope, configuration and + * function/macro signatures may change at any time. We expect a + * stable API for v2. + * + * @note Even if this option is set, some allocations further down + * the call stack will still be made from the stack. Those will + * likely be added to the scope of this option in the future. + * + * @note MLD_CUSTOM_ALLOC need not guarantee a successful + * allocation nor include error handling. Upon failure, the + * target pointer should simply be set to NULL. The calling + * code will handle this case and invoke MLD_CUSTOM_FREE. + */ +/* #define MLD_CONFIG_CUSTOM_ALLOC_FREE + #if !defined(__ASSEMBLER__) + #include + #define MLD_CUSTOM_ALLOC(v, T, N) \ + T* (v) = (T *)aligned_alloc(MLD_DEFAULT_ALIGN, \ + MLD_ALIGN_UP(sizeof(T) * (N))) + #define MLD_CUSTOM_FREE(v, T, N) free(v) + #endif +*/ + +/** + * MLD_CONFIG_CUSTOM_MEMCPY + * + * Set this option and define `mld_memcpy` if you want to + * use a custom method to copy memory instead of the standard + * library memcpy function. + * + * The custom implementation must have the same signature and + * behavior as the standard memcpy function: + * void *mld_memcpy(void *dest, const void *src, size_t n) + */ +#define MLD_CONFIG_CUSTOM_MEMCPY +#if !defined(__ASSEMBLER__) +#include "../mldsa/src/native/armv81m/mve_memory.h" +#endif + + +/** + * MLD_CONFIG_CUSTOM_MEMSET + * + * Set this option and define `mld_memset` if you want to + * use a custom method to set memory instead of the standard + * library memset function. + * + * The custom implementation must have the same signature and + * behavior as the standard memset function: + * void *mld_memset(void *s, int c, size_t n) + */ +#define MLD_CONFIG_CUSTOM_MEMSET +#if !defined(__ASSEMBLER__) +#include "../mldsa/src/native/armv81m/mve_memory.h" +#endif + + +/** + * MLD_CONFIG_INTERNAL_API_QUALIFIER + * + * If set, this option provides an additional qualifier + * to be added to declarations of internal API functions and data. + * + * The primary use case for this option are single-CU builds, + * in which case this option can be set to `static`. + */ +/* #define MLD_CONFIG_INTERNAL_API_QUALIFIER */ + +/** + * MLD_CONFIG_CT_TESTING_ENABLED + * + * If set, mldsa-native annotates data as secret / public using + * valgrind's annotations VALGRIND_MAKE_MEM_UNDEFINED and + * VALGRIND_MAKE_MEM_DEFINED, enabling various checks for secret- + * dependent control flow of variable time execution (depending + * on the exact version of valgrind installed). + */ +/* #define MLD_CONFIG_CT_TESTING_ENABLED */ + +/** + * MLD_CONFIG_NO_ASM + * + * If this option is set, mldsa-native will be built without + * use of native code or inline assembly. + * + * By default, inline assembly is used to implement value barriers. + * Without inline assembly, mldsa-native will use a global volatile + * 'opt blocker' instead; see ct.h. + * + * Inline assembly is also used to implement a secure zeroization + * function on non-Windows platforms. If this option is set and + * the target platform is not Windows, you MUST set + * MLD_CONFIG_CUSTOM_ZEROIZE and provide a custom zeroization + * function. + * + * If this option is set, MLD_CONFIG_USE_NATIVE_BACKEND_FIPS202 and + * and MLD_CONFIG_USE_NATIVE_BACKEND_ARITH will be ignored, and no + * native backends will be used. + */ +/* #define MLD_CONFIG_NO_ASM */ + +/** + * MLD_CONFIG_NO_ASM_VALUE_BARRIER + * + * If this option is set, mldsa-native will be built without + * use of native code or inline assembly for value barriers. + * + * By default, inline assembly (if available) is used to implement + * value barriers. + * Without inline assembly, mldsa-native will use a global volatile + * 'opt blocker' instead; see ct.h. + */ +/* #define MLD_CONFIG_NO_ASM_VALUE_BARRIER */ + +/** + * MLD_CONFIG_KEYGEN_PCT + * + * Compliance with @[FIPS140_3_IG, p.87] requires a + * Pairwise Consistency Test (PCT) to be carried out on a freshly + * generated keypair before it can be exported. + * + * Set this option if such a check should be implemented. + * In this case, keypair_internal and + * keypair will return a non-zero error code if the + * PCT failed. + * + * @note This feature will drastically lower the performance of + * key generation. + * + * @note This option is incompatible with MLD_CONFIG_NO_SIGN_API + * and MLD_CONFIG_NO_VERIFY_API as the current PCT implementation + * requires signature() and verify(). + */ +/* #define MLD_CONFIG_KEYGEN_PCT */ + +/** + * MLD_CONFIG_KEYGEN_PCT_BREAKAGE_TEST + * + * If this option is set, the user must provide a runtime + * function `static inline int mld_break_pct() { ... }` to + * indicate whether the PCT should be made fail. + * + * This option only has an effect if MLD_CONFIG_KEYGEN_PCT is set. + */ +/* #define MLD_CONFIG_KEYGEN_PCT_BREAKAGE_TEST + #if !defined(__ASSEMBLER__) + #include "src/src.h" + static MLD_INLINE int mld_break_pct(void) + { + ... return 0/1 depending on whether PCT should be broken ... + } + #endif +*/ + +/** + * MLD_CONFIG_MAX_SIGNING_ATTEMPTS + * + * Upper bound on the number of rejection-sampling iterations + * performed by ML-DSA signing (@[FIPS204, Algorithm 7]). + * + * If a valid signature is not produced within this many + * attempts, signing returns MLD_ERR_SIGN_ATTEMPTS_EXHAUSTED. + * This is useful in timing-sensitive environments that + * require a deterministic worst-case bound on signing time. + * + * For FIPS 204 compliance, this value MUST be at least 814, + * cf. @[FIPS204, Appendix C], which is chosen so that the + * signing failure rate is < 2^{-256}. + * + * Default: Largest possible value before internal counters + * would overflow. This is larger than the FIPS204 bound. + * + * In particular, in the default configuration, the signing + * failure rate is < 2^{-256}. + */ +/* #define MLD_CONFIG_MAX_SIGNING_ATTEMPTS 814 */ + +/** + * MLD_CONFIG_SERIAL_FIPS202_ONLY + * + * Set this to use a FIPS202 implementation with global state + * that supports only one active Keccak computation at a time + * (e.g. some hardware accelerators). + * + * If this option is set, ML-DSA will use FIPS202 operations + * serially, ensuring that only one SHAKE context is active + * at any given time. + * + * This allows offloading Keccak computations to a hardware + * accelerator that holds only a single Keccak state locally, + * rather than requiring support for multiple concurrent + * Keccak states. + * + * @note Depending on the target CPU, this may reduce + * performance when using software FIPS202 implementations. + * Only enable this when you have to. + */ +/* #define MLD_CONFIG_SERIAL_FIPS202_ONLY */ + +/** + * MLD_CONFIG_CONTEXT_PARAMETER + * + * Set this to add a context parameter that is provided to public + * API functions and is then available in custom callbacks. + * + * The type of the context parameter is configured via + * MLD_CONFIG_CONTEXT_PARAMETER_TYPE. + */ +/* #define MLD_CONFIG_CONTEXT_PARAMETER */ + +/** + * MLD_CONFIG_CONTEXT_PARAMETER_TYPE + * + * Set this to define the type for the context parameter used by + * MLD_CONFIG_CONTEXT_PARAMETER. + * + * This is only relevant if MLD_CONFIG_CONTEXT_PARAMETER is set. + */ +/* #define MLD_CONFIG_CONTEXT_PARAMETER_TYPE void* */ + +/** + * MLD_CONFIG_REDUCE_RAM + * + * Set this to reduce RAM usage. This trades memory for performance. + * + * For expected memory usage, see the MLD_TOTAL_ALLOC_* constants defined in + * mldsa_native.h. + * + * This option is useful for embedded systems with tight RAM constraints but + * relaxed performance requirements. + * + */ +/* #define MLD_CONFIG_REDUCE_RAM */ + +/************************* Config internals ********************************/ + +#endif /* MLD_BUILD_INTERNAL */ + +/* Default namespace + * + * Don't change this. If you need a different namespace, re-define + * MLD_CONFIG_NAMESPACE_PREFIX above instead, and remove the following. + * + * The default MLDSA namespace is + * + * PQCP_MLDSA_NATIVE_MLDSA_ + * + * e.g., PQCP_MLDSA_NATIVE_MLDSA44_ + */ + +#if MLD_CONFIG_PARAMETER_SET == 44 +#define MLD_DEFAULT_NAMESPACE_PREFIX PQCP_MLDSA_NATIVE_MLDSA44 +#elif MLD_CONFIG_PARAMETER_SET == 65 +#define MLD_DEFAULT_NAMESPACE_PREFIX PQCP_MLDSA_NATIVE_MLDSA65 +#elif MLD_CONFIG_PARAMETER_SET == 87 +#define MLD_DEFAULT_NAMESPACE_PREFIX PQCP_MLDSA_NATIVE_MLDSA87 +#endif + +#endif /* !MLD_CONFIG_H */ diff --git a/test/configs/configs.yml b/test/configs/configs.yml index e1b364061f..196a84c287 100644 --- a/test/configs/configs.yml +++ b/test/configs/configs.yml @@ -146,6 +146,28 @@ configs: } #endif + - path: test/configs/armv81m_mve_memory_config.h + description: "Test configuration with Armv8.1-M MVE custom zeroize, memcpy, and memset" + defines: + MLD_CONFIG_CUSTOM_ZEROIZE: + content: | + #define MLD_CONFIG_CUSTOM_ZEROIZE + #if !defined(__ASSEMBLER__) + #include "../mldsa/src/native/armv81m/mve_memory.h" + #endif + MLD_CONFIG_CUSTOM_MEMCPY: + content: | + #define MLD_CONFIG_CUSTOM_MEMCPY + #if !defined(__ASSEMBLER__) + #include "../mldsa/src/native/armv81m/mve_memory.h" + #endif + MLD_CONFIG_CUSTOM_MEMSET: + content: | + #define MLD_CONFIG_CUSTOM_MEMSET + #if !defined(__ASSEMBLER__) + #include "../mldsa/src/native/armv81m/mve_memory.h" + #endif + - path: test/configs/break_pct_config.h description: "Test configuration for PCT breakage testing" defines: diff --git a/test/hal/hal.c b/test/hal/hal.c index 01d00e117a..ddd0133867 100644 --- a/test/hal/hal.c +++ b/test/hal/hal.c @@ -50,7 +50,7 @@ void enable_cyclecounter(void) {} void disable_cyclecounter(void) {} -uint64_t get_cyclecounter(void) { return k_cycle_get_32(); } +uint64_t get_cyclecounter(void) { return k_cycle_get_64(); } #elif defined(PMU_CYCLES) @@ -125,8 +125,8 @@ uint64_t get_cyclecounter(void) { return DWT->CYCCNT; } #elif defined(ARMCM55) /* Cortex-M55: Use dedicated PMU */ #include +#include #include -#include "pmu_armv8.h" void enable_cyclecounter(void) { diff --git a/test/mk/components.mk b/test/mk/components.mk index 3a814a818e..52ba35b2e0 100644 --- a/test/mk/components.mk +++ b/test/mk/components.mk @@ -154,6 +154,9 @@ $(call MAKE_OBJS, $(MLDSA87_DIR), $(EXTRA_SOURCES)): CFLAGS += $(EXTRA_SOURCES_C endif else $(ALL_BINS): $$(TEST_SRCS) $(LIB_SRCS) +# Extra per-binary prerequisites a custom-build platform needs (e.g. Zephyr's +# app inputs and active-target marker, set in test/zephyr/platform.mk). +$(ALL_BINS): $(CUSTOM_BUILD_DEPS) endif # ABI checker diff --git a/test/zephyr/README.md b/test/zephyr/README.md index 480bbada91..f47204566f 100644 --- a/test/zephyr/README.md +++ b/test/zephyr/README.md @@ -4,8 +4,9 @@ This is a test platform that builds the mldsa-native test applications as [Zephyr](https://www.zephyrproject.org/) applications, so they can run on -QEMU-emulated Arm MPS boards. It covers Cortex-M3/M4/M7/M33/M55 through a -single platform, without the need for per-board hardware abstraction layers. +QEMU-emulated Arm MPS boards and the NUCLEO-N657X0-Q hardware board. It covers +Cortex-M3/M4/M7/M33/M55 through a single platform, without the need for +per-board hardware abstraction layers. ## Usage @@ -23,16 +24,17 @@ nix develop .#zephyr --command ./scripts/tests func --opt=opt Currently supported targets: -| `ZEPHYR_TARGET` | Zephyr board | QEMU machine | Core | -| --------------- | --------------------- | ------------ | ---------- | -| `mps2-an385` | `mps2/an385` | `mps2-an385` | Cortex-M3 | -| `mps2-an386` | `mps2/an386` | `mps2-an386` | Cortex-M4 | -| `mps2-an500` | `mps2/an500` | `mps2-an500` | Cortex-M7 | -| `mps2-an521` | `mps2/an521/cpu0` | `mps2-an521` | Cortex-M33 | -| `mps3-an547` | `mps3/corstone300/an547` | `mps3-an547` | Cortex-M55 | +| `ZEPHYR_TARGET` | Zephyr board | Runner | Core | +| ------------------- | ------------------------ | -------------- | ---------- | +| `mps2-an385` | `mps2/an385` | `mps2-an385` | Cortex-M3 | +| `mps2-an386` | `mps2/an386` | `mps2-an386` | Cortex-M4 | +| `mps2-an500` | `mps2/an500` | `mps2-an500` | Cortex-M7 | +| `mps2-an521` | `mps2/an521/cpu0` | `mps2-an521` | Cortex-M33 | +| `mps3-an547` | `mps3/corstone300/an547` | `mps3-an547` | Cortex-M55 | +| `nucleo-n657x0-q` | `nucleo_n657x0_q` | OpenOCD + GDB | Cortex-M55 | The Armv8.1-M MVE FIPS202 backend is an `OPT=1` feature and is built for -`mps3-an547` only (the only listed core with MVE). +`mps3-an547` and `nucleo-n657x0-q`. ## How it works diff --git a/test/zephyr/app/CMakeLists.txt b/test/zephyr/app/CMakeLists.txt index c426bc2c9c..914ee0e7c3 100644 --- a/test/zephyr/app/CMakeLists.txt +++ b/test/zephyr/app/CMakeLists.txt @@ -21,7 +21,6 @@ list(TRANSFORM _test_srcs PREPEND ${R}/) target_sources(app PRIVATE ${R}/mldsa/mldsa_native.c ${_test_srcs} - ${CMAKE_CURRENT_SOURCE_DIR}/shim.c ) target_include_directories(app PRIVATE @@ -30,6 +29,31 @@ target_include_directories(app PRIVATE ${R}/test/hal ) +# The Zephyr shim owns main() (see below). On QEMU targets shim.c stops the +# emulator with the test's exit code; the NUCLEO-N657X0-Q hardware target needs +# its own shim plus libc wrappers that route stdio over ITM/SWO. +if(ZEPHYR_NUCLEO_N657X0_Q) + target_sources(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/shim_nucleo_n657x0_q.c) + target_compile_definitions(app PRIVATE + printf=__wrap_printf + fprintf=__wrap_fprintf + puts=__wrap_puts + putchar=__wrap_putchar + fflush=__wrap_fflush + exit=__wrap_exit + ) + target_link_options(app PRIVATE + -Wl,--wrap=printf + -Wl,--wrap=fprintf + -Wl,--wrap=puts + -Wl,--wrap=putchar + -Wl,--wrap=fflush + -Wl,--wrap=exit + ) +else() + target_sources(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/shim.c) +endif() + # The test binary's CFLAGS, forwarded from the build (test/zephyr/platform.mk # assembles and escapes them) and applied to the mldsa amalgamation and the test # sources alike, so both honour the project's own warning/opt/std policy and @@ -47,6 +71,9 @@ endif() set_source_files_properties(${R}/test/hal/hal.c PROPERTIES COMPILE_OPTIONS "-Wno-error") +set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/shim_nucleo_n657x0_q.c + PROPERTIES COMPILE_OPTIONS "-Wno-error") + # Optional native FIPS202 backend (e.g. Armv8.1-M MVE on Cortex-M55). The # monolithic mldsa_native.c already includes the backend C sources; its # assembly counterpart comes from mldsa_native_asm.S. @@ -57,9 +84,10 @@ if(ZEPHYR_FIPS202_BACKEND) "MLD_CONFIG_FIPS202_BACKEND_FILE=\"${ZEPHYR_FIPS202_BACKEND}\"") endif() -# Each test brings its own int main(void); rename it so the Zephyr shim -# (shim.c) owns main() and can stop QEMU with the test's exit code. Only the -# test entrypoint in ZEPHYR_TEST_SRCS defines main(); the support sources -# (notrandombytes, hal) don't, so applying the define to all is harmless. +# Each test brings its own int main(void); rename it so the selected Zephyr +# shim owns main() and can return the test's exit code through the target's +# runner. Only the test entrypoint in ZEPHYR_TEST_SRCS defines main(); the +# support sources (notrandombytes, hal) don't, so applying the define to all is +# harmless. set_source_files_properties(${_test_srcs} PROPERTIES COMPILE_DEFINITIONS "main=mld_test_main") diff --git a/test/zephyr/app/nucleo_n657x0_q.conf b/test/zephyr/app/nucleo_n657x0_q.conf new file mode 100644 index 0000000000..a517a7a0d4 --- /dev/null +++ b/test/zephyr/app/nucleo_n657x0_q.conf @@ -0,0 +1,25 @@ +# Copyright (c) The mldsa-native project authors +# Copyright (c) The mlkem-native project authors +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +# The board default exposes a 2 MiB SRAM window at 0x34000000; its first +# 400 KiB is FLEXRAM. The NUCLEO runner repurposes FLEXMEM to expand the TCMs +# to 256 KiB ITCM + 256 KiB DTCM, so keep the Zephyr image above that low +# window and leave fixed host buffers after the linked image. +CONFIG_SRAM_BASE_ADDRESS=0x34080000 +CONFIG_SRAM_SIZE=192 + +# The component benchmark app keeps five 8 KiB scratch buffers on the main +# stack; measured max static stack depth is about 49.5 KiB, so 32 KiB is not +# enough for that benchmark. +CONFIG_MAIN_STACK_SIZE=65536 + +# Test stdout is wrapped by shim_nucleo_n657x0_q.c and emitted on ITM/SWO. +CONFIG_STDOUT_CONSOLE=n +CONFIG_UART_CONSOLE=n + +# The host runner restores a dynamic command-line string into the fixed +# bootargs handoff window before Zephyr calls get_bootargs(). +CONFIG_BOOTARGS=y +CONFIG_DYNAMIC_BOOTARGS=y +CONFIG_BOOTARGS_ARGS_BUFFER_SIZE=16384 diff --git a/test/zephyr/app/nucleo_n657x0_q.overlay b/test/zephyr/app/nucleo_n657x0_q.overlay new file mode 100644 index 0000000000..2e87f5e984 --- /dev/null +++ b/test/zephyr/app/nucleo_n657x0_q.overlay @@ -0,0 +1,17 @@ +/* + * Copyright (c) The mldsa-native project authors + * Copyright (c) The mlkem-native project authors + * SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + */ + +/ { + mld_axisram: memory@34080000 { + compatible = "zephyr,memory-region", "mmio-sram"; + reg = <0x34080000 0x00030000>; + zephyr,memory-region = "NUCLEO_AXISRAM"; + }; + + chosen { + zephyr,sram = &mld_axisram; + }; +}; diff --git a/test/zephyr/app/shim_nucleo_n657x0_q.c b/test/zephyr/app/shim_nucleo_n657x0_q.c new file mode 100644 index 0000000000..eebb01685a --- /dev/null +++ b/test/zephyr/app/shim_nucleo_n657x0_q.c @@ -0,0 +1,241 @@ +/* + * Copyright (c) The mldsa-native project authors + * Copyright (c) The mlkem-native project authors + * SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + */ + +/* + * Zephyr entrypoint shim for RAM-loaded NUCLEO-N657X0-Q tests. + * + * The OpenOCD/GDB wrapper breaks at get_bootargs() after Zephyr has cleared + * BSS, restores a Zephyr dynamic bootargs command line into + * mld_bootargs_block, then continues. Zephyr parses the command line and calls + * main(argc, argv). Target stdout/stderr goes through ITM stimulus port 0 and + * is captured by the host over SWO. The test return code is passed to + * nucleo_test_done(), where the host wrapper reads r0. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern int mld_test_main(int argc, char **argv); + +void nucleo_test_breakpoint(int rc); +int __wrap_printf(const char *format, ...); +int __wrap_fprintf(FILE *stream, const char *format, ...); +int __wrap_puts(const char *s); +int __wrap_putchar(int c); +int __wrap_fflush(FILE *stream); +__attribute__((noreturn)) void nucleo_test_done(int rc); +__attribute__((noreturn)) void __wrap_exit(int status); +const char *get_bootargs(void); + +#define BOOTARGS_BLOCK_SIZE (64U * 1024U) +#define NUCLEO_ITM_LAR (*(volatile uint32_t *)0xE0000FB0UL) + +__asm__( + ".global mld_bootargs_block\n" + ".set mld_bootargs_block, 0x340b0000\n"); + +extern char mld_bootargs_block[BOOTARGS_BLOCK_SIZE]; + +static char nucleo_stdout_buf[8192]; +static size_t nucleo_stdout_len; +static bool nucleo_swo_enabled; + +static void nucleo_swo_pin_setup(void) +{ + LL_AHB4_GRP1_EnableClock(LL_AHB4_GRP1_PERIPH_GPIOB); + LL_GPIO_SetPinMode(GPIOB, LL_GPIO_PIN_5, LL_GPIO_MODE_ALTERNATE); + LL_GPIO_SetPinSpeed(GPIOB, LL_GPIO_PIN_5, LL_GPIO_SPEED_FREQ_VERY_HIGH); + LL_GPIO_SetPinPull(GPIOB, LL_GPIO_PIN_5, LL_GPIO_PULL_NO); + LL_GPIO_SetAFPin_0_7(GPIOB, LL_GPIO_PIN_5, LL_GPIO_AF_0); +} + +static void nucleo_swo_enable(void) +{ + if (nucleo_swo_enabled) + { + return; + } + + nucleo_swo_pin_setup(); + LL_BUS_EnableClock(LL_APB3); + LL_DBGMCU_EnableDBGClock(); + LL_DBGMCU_EnableTPIUExportClock(); + CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; + + NUCLEO_ITM_LAR = 0xC5ACCE55UL; + ITM->TER = 0UL; + ITM->TCR &= ~ITM_TCR_ITMENA_Msk; + while ((ITM->TCR & ITM_TCR_BUSY_Msk) != 0UL) + { + } + ITM->TPR = 0UL; + ITM->TCR = + ITM_TCR_ITMENA_Msk | ITM_TCR_DWTENA_Msk | (1UL << ITM_TCR_TRACEBUSID_Pos); + ITM->TER = 1UL; + nucleo_swo_enabled = true; +} + +static void nucleo_swo_write_byte(char ch) +{ + nucleo_swo_enable(); + + if (((ITM->TCR & ITM_TCR_ITMENA_Msk) == 0UL) || ((ITM->TER & 1UL) == 0UL)) + { + return; + } + + while (ITM->PORT[0U].u32 == 0UL) + { + } + ITM->PORT[0U].u8 = (uint8_t)ch; +} + +static void nucleo_swo_write_raw(const char *src, size_t len) +{ + for (size_t offset = 0; offset < len; offset++) + { + nucleo_swo_write_byte(src[offset]); + } +} + +static void nucleo_swo_wait_idle(void) +{ + for (uint32_t timeout = 10000000UL; timeout > 0U; timeout--) + { + if ((ITM->TCR & ITM_TCR_BUSY_Msk) == 0UL) + { + break; + } + } + __DSB(); +} + +__attribute__((noinline, used)) void nucleo_test_breakpoint(int rc) +{ + (void)rc; + __asm__ volatile("bkpt 0" ::: "memory"); +} + +static void nucleo_stdout_flush(void) +{ + if (nucleo_stdout_len == 0U) + { + return; + } + + nucleo_swo_write_raw(nucleo_stdout_buf, nucleo_stdout_len); + nucleo_stdout_len = 0U; +} + +static void nucleo_stdout_write(const char *src, size_t len) +{ + for (size_t i = 0; i < len; i++) + { + nucleo_stdout_buf[nucleo_stdout_len++] = src[i]; + if (src[i] == '\n' || nucleo_stdout_len == sizeof(nucleo_stdout_buf)) + { + nucleo_stdout_flush(); + } + } +} + +static int nucleo_vprintf(const char *format, va_list ap) +{ + char buf[8192]; + int rc = vsnprintf(buf, sizeof(buf), format, ap); + + if (rc <= 0) + { + return rc; + } + + nucleo_stdout_write( + buf, (size_t)((rc < (int)sizeof(buf)) ? rc : (int)sizeof(buf) - 1)); + return rc; +} + +int __wrap_printf(const char *format, ...) +{ + va_list ap; + int rc; + + va_start(ap, format); + rc = nucleo_vprintf(format, ap); + va_end(ap); + return rc; +} + +int __wrap_fprintf(FILE *stream, const char *format, ...) +{ + va_list ap; + int rc; + + (void)stream; + va_start(ap, format); + rc = nucleo_vprintf(format, ap); + va_end(ap); + return rc; +} + +int __wrap_puts(const char *s) +{ + size_t len = strlen(s); + + nucleo_stdout_write(s, len); + nucleo_stdout_write("\n", 1); + return (int)len + 1; +} + +int __wrap_putchar(int c) +{ + char ch = (char)c; + + nucleo_stdout_write(&ch, 1); + return c; +} + +int __wrap_fflush(FILE *stream) +{ + (void)stream; + nucleo_stdout_flush(); + return 0; +} + +__attribute__((noreturn, noinline, used)) void nucleo_test_done(int rc) +{ + nucleo_stdout_flush(); + nucleo_swo_wait_idle(); + nucleo_test_breakpoint(rc); + for (;;) + { + __WFI(); + } +} + +__attribute__((noreturn)) void __wrap_exit(int status) +{ + nucleo_test_done(status); +} + +__attribute__((noinline, used)) const char *get_bootargs(void) +{ + return mld_bootargs_block; +} + +int main(int argc, char **argv) +{ + int rc = mld_test_main(argc, argv); + + nucleo_test_done(rc); +} diff --git a/test/zephyr/nucleo_n657x0_q/README.md b/test/zephyr/nucleo_n657x0_q/README.md new file mode 100644 index 0000000000..3c79861b00 --- /dev/null +++ b/test/zephyr/nucleo_n657x0_q/README.md @@ -0,0 +1,115 @@ + + +# NUCLEO-N657X0-Q Zephyr Hardware Helpers + +This directory contains the NUCLEO-N657X0-Q Zephyr hardware entry point and +supporting host/debug helpers. The board is not flashed: OpenOCD first expands +FLEXMEM with direct register writes, then GDB loads the Zephyr ELF into RAM. + +Only two target-specific behaviors remain here: + +- FLEXMEM expansion for the STM32N657X0 ITCM/DTCM layout. +- GDB bootargs insertion after Zephyr startup reaches `get_bootargs()`. + +Test output uses ITM stimulus port 0 and OpenOCD SWO capture. The host wrapper +decodes that SWO stream and keeps OpenOCD/GDB control chatter out of stdout +unless `--verbose` is passed. + +## Files + +- `exec_wrapper.py`: configures FLEXMEM, starts OpenOCD with SWO capture + enabled, loads a Zephyr ELF with GDB, restores the dynamic bootargs string, + decodes ITM port 0 output, and reads the target return code at + `nucleo_test_done`. +- `nucleo_host/`: Python helpers for FLEXMEM configuration, OpenOCD command + generation, GDB script generation, symbol lookup, fault diagnostics. + +The Zephyr application shim lives in `test/zephyr/app/shim_nucleo_n657x0_q.c`. + +## Run + +Use the Zephyr platform makefile and select the NUCLEO target: + +```sh +nix develop .#zephyr +make run_func_44 EXTRA_MAKEFILE=test/zephyr/platform.mk ZEPHYR_TARGET=nucleo-n657x0-q +``` + +The execution wrapper configures FLEXMEM before each ELF run. The working +Zephyr hardware run targets are: + +```text +run_kat run_func run_unit run_alloc run_rng_fail run_acvp run_wycheproof +``` + +Known exceptions: + +- `run_stack` is a host-side stack analysis target. In this platform mode it + builds a Zephyr Arm ELF, then the stack script attempts to execute that ELF + on the host. +- `run_abicheck` is not currently built as a NUCLEO Zephyr application when + `OPT=1`; the NUCLEO wrapper expects the Zephyr shim symbols used for bootargs + injection and completion reporting. + +Benchmark targets remain explicit and require the usual `CYCLES` setting: + +```sh +make run_bench_44 CYCLES=NO EXTRA_MAKEFILE=test/zephyr/platform.mk ZEPHYR_TARGET=nucleo-n657x0-q +make run_bench_components_44 CYCLES=NO EXTRA_MAKEFILE=test/zephyr/platform.mk ZEPHYR_TARGET=nucleo-n657x0-q +``` + +Useful environment variables: + +```sh +export OPENOCD_SPEED=8000 +export OPENOCD_SERIAL= +export GDB_PORT=3333 +export SWO_TRACECLK=100000000 +export SWO_PIN_FREQ=1000000 +``` + +`OPENOCD`, `OPENOCD_INTERFACE`, `OPENOCD_TARGET`, `OPENOCD_TRANSPORT`, `GDB`, +`NM`, and `READELF` can override the default tools and OpenOCD scripts. + +## FLEXMEM Sequence + +Zephyr's default NUCLEO-N657X0-Q RAM region is a 2 MiB SRAM window starting at +`0x34000000`; its first 400 KiB is the STM32N657X0 FLEXRAM allocation. After +reset, the Cortex-M55 TCM layout is 64 KiB ITCM and 128 KiB DTCM. The NUCLEO +runner repurposes FLEXMEM to expand the TCMs to 256 KiB ITCM and 256 KiB DTCM +before each RAM-loaded Zephyr test, so the test image is linked above that low +AXISRAM/FLEXRAM window instead of using the board default: + +1. OpenOCD attaches with `reset_config none`. +2. The script enables the SYSCFG clock by setting bit 0 in `RCC_APB4ENSR2` at + `0x56028a78`. +3. It read-modify-writes `SYSCFG_CM55TCMCR` at `0x56008008` so the low byte is + `0x99`. +4. It sets bit 0 in `SYSCFG_CM55RSTCR` at `0x56008018`. +5. It polls until `(SYSCFG_CM55TCMCR & 0xff) == 0x99`. +6. It runs `reset run` so the expanded layout is applied before the Zephyr ELF + is loaded. + +The board is RAM-loaded, not flashed. Zephyr's default NUCLEO-N657X0-Q RAM +region is a 2 MiB SRAM window starting at `0x34000000`; its first 400 KiB is +the STM32N657X0 FLEXRAM allocation. The hardware benchmark setup is the reason +this target uses a board-specific memory layout: before each run, the hardware +wrapper repurposes FLEXMEM to expand the Cortex-M55 TCMs by setting the low +byte of `SYSCFG_CM55TCMCR` to `0x99`, selecting 256 KiB ITCM and 256 KiB DTCM, +then resets the target so that layout is active. Because this changes the low +AXISRAM/FLEXRAM window that the default Zephyr layout would use, the test image +is instead linked in a 192 KiB AXISRAM window at `0x34080000`; see +`nucleo_n657x0_q/README.md` for the exact register sequence. + +Test stdout is captured over ITM stimulus port 0 through SWO. This avoids using +semihosting for normal output: semihosting is blocking and each host operation +requires debug exception/interrupt handling, which makes it too slow and too +intrusive for repeated hardware test and benchmark runs. SWO is also simpler +than UART in this setup because the OpenOCD SWO endpoint is tied directly to +the selected debug probe, while UART output requires identifying the correct +virtual COM port for that specific probe. diff --git a/test/zephyr/nucleo_n657x0_q/exec_wrapper.py b/test/zephyr/nucleo_n657x0_q/exec_wrapper.py new file mode 100755 index 0000000000..9cf3f880af --- /dev/null +++ b/test/zephyr/nucleo_n657x0_q/exec_wrapper.py @@ -0,0 +1,526 @@ +#!/usr/bin/env python3 +# Copyright (c) The mldsa-native project authors +# Copyright (c) The mlkem-native project authors +# Copyright (c) Arm Ltd. +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +""" +Run one RAM-resident NUCLEO-N657X0-Q Zephyr test ELF through OpenOCD. + +The wrapper first expands ITCM/DTCM with the direct FLEXMEM OpenOCD script. It +then starts OpenOCD with SWO trace redirected to a local TCP listener, +GDB-loads the Zephyr ELF into RAM, restores a dynamic bootargs string after +Zephyr startup reaches ``get_bootargs``, decodes target stdout from ITM port 0, +and reads the target return code from ``r0`` at the ``nucleo_test_done`` +breakpoint. +""" + +import logging +import os +import re +import select +import socket +import subprocess +import sys +import tempfile +import time + +from nucleo_host.flexmem_configure import run_openocd_config as run_flexmem_config +from nucleo_host.gdb_script import build_run_script +from nucleo_host.openocd_tools import find_openocd +from nucleo_host.openocd_tools import runtime_gdbserver_cmd +from nucleo_host.openocd_tools import serial_from_env +from nucleo_host.openocd_tools import speed_khz_from_env +from nucleo_host.openocd_tools import swo_formatter_from_env +from nucleo_host.openocd_tools import swo_pin_freq_from_env +from nucleo_host.openocd_tools import swo_traceclk_from_env +from nucleo_host.openocd_tools import transport_from_env +from nucleo_host.results import fault_info_from_gdb +from nucleo_host.results import gdb_observed_hardfault +from nucleo_host.symbols import default_readelf +from nucleo_host.symbols import resolve_symbol + +VERBOSE = False +LOG = logging.getLogger(__name__) +BOOTARGS_BLOCK_SIZE = 64 * 1024 + + +def _quote_bootarg(arg): + """ + Quote one argument for Zephyr's boot_args.c parser. + + Zephyr groups a token when it starts with a matching single or double quote, + and strips the quote characters. It does not implement shell-style escapes. + """ + if arg == "" or arg[0] in ("'", '"') or any(ch.isspace() for ch in arg): + if "'" not in arg: + return f"'{arg}'" + if '"' not in arg: + return f'"{arg}"' + raise ValueError( + "Zephyr bootargs cannot represent arguments that need quoting " + "and contain both quote characters" + ) + return arg + + +def _pack_bootargs(args, block_size=BOOTARGS_BLOCK_SIZE): + """ + Return a padded UTF-8 dynamic bootargs command line for Zephyr. + + The NUCLEO runner restores this string into the fixed target reservation + before Zephyr calls ``get_bootargs()``. The result is padded to the full + reservation so GDB ``restore`` overwrites stale contents from prior runs. + """ + cmdline = " ".join(_quote_bootarg(arg) for arg in args).encode("utf-8") + b"\x00" + if len(cmdline) > block_size: + raise ValueError( + f"bootargs string is {len(cmdline)} bytes, exceeds {block_size}-byte block" + ) + return cmdline + bytes(block_size - len(cmdline)) + + +def configure_logging(): + """Configure process-wide logging after ``VERBOSE`` has been parsed.""" + level = logging.DEBUG if VERBOSE else logging.INFO + logging.basicConfig(level=level, format="%(message)s") + + +def log_output(output, level=logging.INFO, prefix=None): + """Log multiline subprocess output one line at a time.""" + if not output: + return + for line in str(output).rstrip().splitlines(): + if prefix: + line = f"{prefix}{line}" + LOG.log(level, "%s", line) + + +def err(msg): + """Report an error message regardless of verbose mode.""" + log_output(msg, logging.ERROR) + + +def info(msg): + """Report an informational message only in verbose mode.""" + if VERBOSE: + LOG.debug("%s", msg) + + +def popen(cmd, **kwargs): + """Wrap ``subprocess.Popen`` for test-time monkeypatching.""" + return subprocess.Popen(cmd, **kwargs) + + +def _reserve_localhost_port(): + """Return an available localhost TCP port for OpenOCD to listen on.""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.bind(("127.0.0.1", 0)) + return sock.getsockname()[1] + + +def _emit_target_bytes(data: bytes): + """Forward decoded target stdout/stderr bytes to host stdout.""" + if not data: + return + out = getattr(sys.stdout, "buffer", None) + if out is not None: + out.write(data) + out.flush() + else: + sys.stdout.write(data.decode("utf-8", errors="replace")) + sys.stdout.flush() + + +def _decode_swo_itm(state, data: bytes): + """Extract software stimulus port 0 bytes from raw ITM trace packets.""" + out = bytearray() + remaining = state.get("itm_remaining", 0) + emit = state.get("itm_emit", False) + + for byte in data: + if remaining: + if emit: + out.append(byte) + remaining -= 1 + continue + + size_code = byte & 0x03 + if size_code == 0: + continue + + remaining = 4 if size_code == 3 else size_code + port = (byte >> 3) & 0x1F + is_software_packet = (byte & 0x04) == 0 + emit = is_software_packet and port == 0 + + state["itm_remaining"] = remaining + state["itm_emit"] = emit + return bytes(out) + + +def _drain_swo(state): + """Connect to and drain OpenOCD SWO TCP output without blocking.""" + conn = state.get("conn") + if conn is None: + try: + conn = socket.create_connection(("127.0.0.1", state["port"]), timeout=0.01) + conn.setblocking(False) + state["conn"] = conn + except OSError: + return + + conn = state.get("conn") + if conn is None: + return + + while True: + try: + readable, _, _ = select.select([conn], [], [], 0) + if not readable: + return + data = conn.recv(4096) + except OSError: + state["conn"] = None + return + if not data: + try: + conn.close() + except OSError: + pass + state["conn"] = None + return + decoded = _decode_swo_itm(state, data) + state["swo_raw_bytes"] = state.get("swo_raw_bytes", 0) + len(data) + _emit_target_bytes(decoded) + + +def _wait_swo_connected(state, timeout_s=3.0): + """Connect to OpenOCD's SWO TCP listener before target execution.""" + deadline = time.time() + timeout_s + while time.time() < deadline: + _drain_swo(state) + if state.get("conn") is not None: + return True + time.sleep(0.05) + return False + + +def _close_swo_state(state): + """Close SWO connection handles.""" + conn = state.get("conn") + if conn is not None: + try: + conn.close() + except OSError: + pass + + +def _drain_swo_until_idle(state, idle_s=0.5, timeout_s=5.0): + """Drain delayed SWO bytes until the stream is briefly idle.""" + deadline = time.time() + timeout_s + idle_deadline = time.time() + idle_s + last_raw_bytes = state.get("swo_raw_bytes", 0) + + while time.time() < deadline: + _drain_swo(state) + raw_bytes = state.get("swo_raw_bytes", 0) + if raw_bytes != last_raw_bytes: + last_raw_bytes = raw_bytes + idle_deadline = time.time() + idle_s + elif time.time() >= idle_deadline: + break + time.sleep(0.01) + + +def _parse_exit_code(gdb_text: str): + """Return the target exit code printed by the GDB script, if present.""" + match = re.search(r"^NUCLEO_EXIT_CODE=(-?\d+)$", gdb_text, re.MULTILINE) + if match: + return int(match.group(1)) + return None + + +def _run_once(): + """Run the target ELF once and return its wrapper exit code.""" + global VERBOSE + + argv = sys.argv[1:] + if "--verbose" in argv: + VERBOSE = True + argv.remove("--verbose") + if "-v" in argv: + VERBOSE = True + argv.remove("-v") + + configure_logging() + + if len(argv) < 1: + err("Usage: exec_wrapper.py [--verbose] [args...]") + return 2 + + elf = os.path.abspath(argv[0]) + args = argv # Preserve the existing convention: argv[0] is the ELF path. + + if not os.path.exists(elf): + err(f"ELF not found: {elf}") + return 2 + + gdb = os.environ.get("GDB", "arm-none-eabi-gdb") + nm = os.environ.get("NM", "arm-none-eabi-nm") + readelf = os.environ.get("READELF", default_readelf()) + port = int(os.environ.get("GDB_PORT", "3333")) + gdb_run_timeout = float(os.environ.get("GDB_RUN_TIMEOUT", "180")) + + arg_block_sym = "mld_bootargs_block" + arg_block_addr = None + + def _resolve_symbol_addr(elf_path: str, sym: str): + return resolve_symbol(elf_path, sym, nm=nm, readelf=readelf) + + def _resolve_first_symbol(elf_path: str, symbols): + for sym in symbols: + addr = _resolve_symbol_addr(elf_path, sym) + if addr is not None: + return sym, addr + return symbols[0], None + + for cand in (arg_block_sym, "mldsa_bootargs_block"): + addr = _resolve_symbol_addr(elf, cand) + if addr is not None: + arg_block_sym = cand + arg_block_addr = addr + break + + bootargs_sym, bootargs_addr = _resolve_first_symbol(elf, ["get_bootargs"]) + bootargs_break = bootargs_sym + if bootargs_addr is not None: + bootargs_break = f"*{bootargs_addr}" + + done_sym, done_addr = _resolve_first_symbol( + elf, ["nucleo_test_breakpoint", "nucleo_test_done"] + ) + done_break = done_sym + if done_addr is not None: + done_break = f"*{done_addr}" + + hardfault_sym, hardfault_addr = _resolve_first_symbol( + elf, ["HardFault_Handler", "z_arm_hard_fault"] + ) + hardfault_break = hardfault_sym + if hardfault_addr is not None: + hardfault_break = f"*{hardfault_addr}" + + reset_handler_sym, reset_handler_addr = _resolve_first_symbol( + elf, ["Reset_Handler", "z_arm_reset"] + ) + reset_handler_jump = reset_handler_sym + if reset_handler_addr is not None: + reset_handler_jump = f"*{hex(int(reset_handler_addr, 16) | 1)}" + if reset_handler_addr is None: + err("Failed to resolve Reset_Handler/z_arm_reset in ELF.") + return 2 + + base_addr = None + if arg_block_addr: + try: + base_addr = int(arg_block_addr, 16) + except ValueError: + base_addr = None + + if base_addr is None: + err( + "Failed to resolve base address of bootargs block " + "(mld_bootargs_block/mldsa_bootargs_block)." + ) + err("- Ensure symbols are present in ELF.") + return 2 + + try: + blob = _pack_bootargs(args) + except ValueError as exc: + err(str(exc)) + return 2 + + with tempfile.TemporaryDirectory() as td: + argv_bin = os.path.join(td, "argv.bin") + with open(argv_bin, "wb") as f: + f.write(blob) + + flexmem_timeout_s = float(os.environ.get("FLEXMEM_CONFIG_TIMEOUT", "5")) + info("[exec_wrapper] configuring FLEXMEM...") + flexmem_rc = run_flexmem_config(flexmem_timeout_s) + if flexmem_rc != 0: + return flexmem_rc + + swo_port = _reserve_localhost_port() + swo_state = {"conn": None, "port": swo_port} + openocd = find_openocd(os.environ.get("OPENOCD", "")) + if openocd is None: + _close_swo_state(swo_state) + err("OpenOCD not found; set OPENOCD or ensure openocd is on PATH") + return 2 + gdbserver_cmd = runtime_gdbserver_cmd( + openocd=openocd, + port=port, + swo_port=swo_port, + swo_traceclk=swo_traceclk_from_env(), + swo_pin_freq=swo_pin_freq_from_env(), + swo_formatter=swo_formatter_from_env(), + speed=speed_khz_from_env(), + serial=serial_from_env(), + transport=transport_from_env(), + ) + + info(f"[exec_wrapper] starting OpenOCD on port {port}...") + info(f"[exec_wrapper] SWO redirect port {swo_port}") + info(f"[exec_wrapper] {' '.join(gdbserver_cmd)}") + stp = popen( + gdbserver_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, + universal_newlines=True, + ) + + try: + time.sleep(0.8) + + if stp.poll() is not None: + out_rem = stp.stdout.read() if stp.stdout else "" + if out_rem: + log_output(out_rem, logging.DEBUG if VERBOSE else logging.ERROR) + return 2 + if not _wait_swo_connected(swo_state): + err("FAIL!") + err("failed to connect to OpenOCD SWO TCP listener") + return 2 + + gdb_lines = build_run_script( + port=port, + bootargs_break=bootargs_break, + reset_handler_jump=reset_handler_jump, + hardfault_break=hardfault_break, + done_break=done_break, + argv_bin=argv_bin, + arg_block_addr=arg_block_addr, + arg_block_sym=arg_block_sym, + ) + + if VERBOSE: + LOG.debug("============ GDB SCRIPT ============") + log_output("\n".join(gdb_lines), logging.DEBUG) + LOG.debug("====================================") + + with tempfile.NamedTemporaryFile("w", delete=False, suffix=".gdb") as gs: + for line in gdb_lines: + gs.write(line + "\n") + gdb_script_path = gs.name + + gdb_cmd = [gdb, "--batch", "-x", gdb_script_path, elf] + + info("[exec_wrapper] running gdb batch") + gdbp = popen( + gdb_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + gdb_deadline = ( + time.time() + gdb_run_timeout if gdb_run_timeout > 0 else None + ) + + while True: + _drain_swo(swo_state) + if stp.stdout is not None: + try: + r, _, _ = select.select([stp.stdout], [], [], 0.1) + if r: + line = stp.stdout.readline() + if line and VERBOSE: + log_output(line, logging.DEBUG) + except Exception: + pass + if gdbp.poll() is not None: + break + if gdb_deadline is not None and time.time() > gdb_deadline: + err("FAIL!") + err(f"gdb batch timed out after {gdb_run_timeout:.0f}s") + try: + gdbp.terminate() + gdbp.wait(timeout=1.0) + except Exception: + try: + gdbp.kill() + except Exception: + pass + try: + out, errout = gdbp.communicate(timeout=1.0) + if out: + log_output(out, logging.ERROR) + if errout: + log_output(errout, logging.ERROR) + except Exception: + pass + return 124 + + out, errout = gdbp.communicate() + _drain_swo_until_idle(swo_state) + if out and VERBOSE: + log_output(out, logging.DEBUG) + if errout and VERBOSE: + log_output(errout, logging.DEBUG) + + gdb_text = f"{out}\n{errout}" + exit_code = _parse_exit_code(gdb_text) + hardfaulted = gdb_observed_hardfault(gdb_text) + + if exit_code is not None: + return exit_code + + if hardfaulted: + fault_info = fault_info_from_gdb(gdb_text) + err("FAIL!") + err("Target entered HardFault_Handler") + if fault_info: + err(fault_info) + return 1 + + if gdbp.returncode != 0: + err("FAIL!") + err(f"gdb batch failed with code {gdbp.returncode}") + if out: + log_output(out, logging.ERROR) + if errout: + log_output(errout, logging.ERROR) + return gdbp.returncode + + err("FAIL!") + err("target did not hit nucleo_test_done") + return 1 + + finally: + try: + stp.terminate() + stp.wait(timeout=1.5) + except Exception: + try: + stp.kill() + except Exception: + pass + _close_swo_state(swo_state) + try: + if "gdb_script_path" in locals(): + os.unlink(gdb_script_path) + except Exception: + pass + + +def main(): + """Run the wrapper once with the configured debugger transport.""" + return _run_once() + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test/zephyr/nucleo_n657x0_q/nucleo_host/flexmem_configure.py b/test/zephyr/nucleo_n657x0_q/nucleo_host/flexmem_configure.py new file mode 100755 index 0000000000..7bb3afbbc2 --- /dev/null +++ b/test/zephyr/nucleo_n657x0_q/nucleo_host/flexmem_configure.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +# Copyright (c) The mldsa-native project authors +# Copyright (c) The mlkem-native project authors +# Copyright (c) Arm Ltd. +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +""" +Configure STM32N6 FLEXMEM before loading RAM-resident test images. + +The helper writes the STM32N6 FLEXMEM control registers over SWD, polls +``SYSCFG->CM55TCMCR`` until the requested TCM split is visible, then resets the +target so the next ELF can be loaded into the expanded ITCM/DTCM layout. +""" + +import logging +import os +import sys +import tempfile + +from nucleo_host.openocd_tools import find_openocd +from nucleo_host.openocd_tools import flexmem_script_lines +from nucleo_host.openocd_tools import openocd_base_args +from nucleo_host.openocd_tools import run_quiet +from nucleo_host.openocd_tools import serial_from_env +from nucleo_host.openocd_tools import speed_khz_from_env +from nucleo_host.openocd_tools import transport_from_env + +DONE = "FLEXMEM configuration complete; reset target and load test binary." + +# Polling the register via SWD proves that the new ITCM/DTCM split latched +# before the next test binary is loaded. +RCC_APB4ENSR2_ADDR = "0x56028a78" +CM55TCMCR_ADDR = "0x56008008" +CM55RSTCR_ADDR = "0x56008018" +CM55TCMCR_EXPECTED_MASK = 0xFF +CM55TCMCR_EXPECTED_VALUE = 0x99 + +LOG = logging.getLogger(__name__) + + +def configure_logging(): + """Configure logging, using ``FLEXMEM_VERBOSE`` as the debug switch.""" + level = logging.DEBUG if os.environ.get("FLEXMEM_VERBOSE") else logging.INFO + logging.basicConfig(level=level, format="%(message)s") + + +def log_output(output, level): + """Log multiline subprocess output at the requested level.""" + if not output: + return + for line in output.rstrip().splitlines(): + LOG.log(level, line) + + +def err(msg): + """Report a user-visible error line.""" + LOG.error("%s", msg) + + +def openocd_cli(): + """Return the OpenOCD executable path, or report a helpful error.""" + openocd = find_openocd(os.environ.get("OPENOCD", "")) + if openocd is None: + err("OpenOCD not found; set OPENOCD or ensure openocd is on PATH") + return openocd + + +def _openocd_config_cmd(openocd, timeout_s): + """Build the OpenOCD command for one FLEXMEM configuration attempt.""" + script_lines = flexmem_script_lines( + timeout_ms=int(timeout_s * 1000), + rcc_apb4ensr2_addr=RCC_APB4ENSR2_ADDR, + cm55tcmcr_addr=CM55TCMCR_ADDR, + cm55rstcr_addr=CM55RSTCR_ADDR, + expected_mask=CM55TCMCR_EXPECTED_MASK, + expected_value=CM55TCMCR_EXPECTED_VALUE, + ) + with tempfile.NamedTemporaryFile("w", delete=False, suffix=".cfg") as script: + script.write("\n".join(script_lines)) + script.write("\n") + script_path = script.name + + cmd = openocd_base_args( + openocd=openocd, + speed=speed_khz_from_env(), + serial=serial_from_env(), + transport=transport_from_env(), + ) + ["-f", script_path] + return cmd, script_path + + +def _run_openocd_config_once(openocd, timeout_s): + """Run the OpenOCD FLEXMEM configuration script.""" + cmd, script_path = _openocd_config_cmd(openocd, timeout_s) + try: + cp = run_quiet(cmd) + finally: + try: + os.unlink(script_path) + except OSError: + pass + return cp + + +def run_openocd_config(timeout_s): + """Configure FLEXMEM using OpenOCD memory reads and writes.""" + openocd = openocd_cli() + if openocd is None: + return 2 + + cp = _run_openocd_config_once(openocd, timeout_s) + if os.environ.get("FLEXMEM_VERBOSE") or cp.returncode != 0: + log_output(cp.stdout, logging.DEBUG if cp.returncode == 0 else logging.ERROR) + if cp.returncode != 0: + err("OpenOCD FLEXMEM register configuration failed") + return cp.returncode + + +def main(): + """Configure FLEXMEM and verify the latched layout.""" + configure_logging() + + if len(sys.argv) != 1: + err(f"Usage: {sys.argv[0]}") + return 2 + + timeout_s = float(os.environ.get("FLEXMEM_CONFIG_TIMEOUT", "5")) + + return run_openocd_config(timeout_s) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test/zephyr/nucleo_n657x0_q/nucleo_host/gdb_script.py b/test/zephyr/nucleo_n657x0_q/nucleo_host/gdb_script.py new file mode 100644 index 0000000000..f95f9f0e53 --- /dev/null +++ b/test/zephyr/nucleo_n657x0_q/nucleo_host/gdb_script.py @@ -0,0 +1,121 @@ +# Copyright (c) The mldsa-native project authors +# Copyright (c) The mlkem-native project authors +# Copyright (c) Arm Ltd. +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +"""Generate the GDB batch script used to load and run RAM-resident tests.""" + + +def build_run_script( + *, + port, + bootargs_break, + reset_handler_jump, + hardfault_break, + done_break, + argv_bin, + arg_block_addr, + arg_block_sym, +): + """ + Build the full GDB command list for one test-image run. + + The order is part of the platform contract: load the RAM ELF, run normal + C startup until Zephyr calls ``get_bootargs()``, restore bootargs after + ``.bss`` has been cleared, install completion/fault breakpoints, continue, + then read the target return code from the completion breakpoint. + """ + gdb_lines = [ + "set pagination off", + "set confirm off", + f"target remote localhost:{port}", + # Keep the GDB script focused on target state and RAM transfers. + "load", + f"tbreak {bootargs_break}", + f"jump {reset_handler_jump}", + restore_bootargs_command(argv_bin, arg_block_addr, arg_block_sym), + "set $nucleo_exit_code = -1", + f"break {done_break}", + "commands", + " set $nucleo_exit_code = $r0", + " echo [[NUCLEO-DONE]]\\n", + "end", + f"break {hardfault_break}", + "commands", + " echo [[NUCLEO-HARDFAULT]]\\n", + "end", + ] + gdb_lines += [ + "continue", + "if $nucleo_exit_code != -1", + " echo NUCLEO_EXIT_CODE=", + " output/d $nucleo_exit_code", + " echo \\n", + "end", + ] + gdb_lines += fault_diagnostic_commands() + # Leave the board in a fresh boot state for the next FLEXMEM setup. This + # runs after stdout/fault harvesting and does not affect the current test. + gdb_lines += ["monitor reset_config none", "monitor reset run"] + return gdb_lines + + +def restore_bootargs_command(argv_bin, arg_block_addr, arg_block_sym): + """Return the GDB ``restore`` command for the bootargs string.""" + if arg_block_addr: + # Prefer a numeric address because some RAM-loaded ELFs have unreliable + # symbol lookup after ``target remote``/``load`` transitions. + return f"restore {argv_bin} binary {arg_block_addr}" + return f"restore {argv_bin} binary &{arg_block_sym}" + + +def fault_diagnostic_commands(): + """Return commands that print Cortex-M fault diagnostics.""" + return [ + "info registers", + "x/4wx $sp", + "echo CFSR=", + "output/x *(unsigned int *)0xE000ED28", + "echo \\n", + "echo HFSR=", + "output/x *(unsigned int *)0xE000ED2C", + "echo \\n", + "echo DFSR=", + "output/x *(unsigned int *)0xE000ED30", + "echo \\n", + "echo MMFAR=", + "output/x *(unsigned int *)0xE000ED34", + "echo \\n", + "echo BFAR=", + "output/x *(unsigned int *)0xE000ED38", + "echo \\n", + "echo AFSR=", + "output/x *(unsigned int *)0xE000ED3C", + "echo \\n", + "echo SHCSR=", + "output/x *(unsigned int *)0xE000ED24", + "echo \\n", + "echo CCR=", + "output/x *(unsigned int *)0xE000ED14", + "echo \\n", + "echo MSP=", + "output/x $msp", + "echo \\n", + "echo PSP=", + "output/x $psp", + "echo \\n", + "echo LR=", + "output/x $lr", + "echo \\n", + "echo PC=", + "output/x $pc", + "echo \\n", + "echo STACKED_R0_R1_R2_R3_R12_LR_PC_XPSR:\\n", + "if ($lr & 4)", + " x/8wx $psp", + "else", + " x/8wx $msp", + "end", + "x/4wx 0xE000ED28", + "x/wx 0xE000ED38", + ] diff --git a/test/zephyr/nucleo_n657x0_q/nucleo_host/openocd_tools.py b/test/zephyr/nucleo_n657x0_q/nucleo_host/openocd_tools.py new file mode 100644 index 0000000000..ab0e6e8e11 --- /dev/null +++ b/test/zephyr/nucleo_n657x0_q/nucleo_host/openocd_tools.py @@ -0,0 +1,215 @@ +# Copyright (c) The mldsa-native project authors +# Copyright (c) The mlkem-native project authors +# Copyright (c) Arm Ltd. +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +"""Locate OpenOCD and build NUCLEO-N657X0-Q command lines.""" + +import os +import shutil +import subprocess + + +DEFAULT_INTERFACE = "interface/stlink.cfg" +DEFAULT_TARGET = "target/stm32n6x.cfg" +DEFAULT_CPU_TARGET = "stm32n6x.cpu" +DEFAULT_SWO_TARGET = "stm32n6x.swo" +DEFAULT_TPIU_TARGET = "" + + +def find_openocd(openocd=""): + """Find ``openocd`` from an explicit path or ``PATH``.""" + candidates = [] + if openocd: + candidates.append(openocd) + path_candidate = shutil.which("openocd") + if path_candidate: + candidates.append(path_candidate) + for candidate in candidates: + if candidate and os.path.isfile(candidate) and os.access(candidate, os.X_OK): + return candidate + return None + + +def speed_khz_from_env(default="8000"): + """Return adapter speed in kHz.""" + return os.environ.get("OPENOCD_SPEED", default) + + +def swo_traceclk_from_env(default="100000000"): + """Return the target trace clock in Hz for SWO capture.""" + return os.environ.get("SWO_TRACECLK", default) + + +def swo_pin_freq_from_env(default="1000000"): + """Return the async SWO pin bitrate in Hz.""" + return os.environ.get("SWO_PIN_FREQ", default) + + +def swo_formatter_from_env(default="0"): + """Return the OpenOCD TPIU formatter setting for SWO capture.""" + return os.environ.get("SWO_FORMATTER", default) + + +def swo_target_from_env(default=DEFAULT_SWO_TARGET): + """Return the OpenOCD trace component used for SWO capture.""" + return os.environ.get("OPENOCD_SWO_TARGET", default) + + +def tpiu_target_from_env(default=DEFAULT_TPIU_TARGET): + """Return the OpenOCD TPIU component used to configure target trace output.""" + return os.environ.get("OPENOCD_TPIU_TARGET", default) + + +def serial_from_env(default=""): + """Return the optional OpenOCD adapter serial selector.""" + return os.environ.get("OPENOCD_SERIAL", default) + + +def transport_from_env(default="swd"): + """Return the OpenOCD transport name.""" + return os.environ.get("OPENOCD_TRANSPORT", default).strip().lower() + + +def run_quiet(cmd): + """Run a command with stdout and stderr merged for delayed diagnostics.""" + return subprocess.run( + cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True + ) + + +def openocd_base_args( + *, + openocd="openocd", + interface=None, + target=None, + speed="8000", + serial="", + transport="swd", +): + """Return common OpenOCD arguments for the NUCLEO debug connection.""" + args = [ + openocd, + "-f", + interface or os.environ.get("OPENOCD_INTERFACE", DEFAULT_INTERFACE), + "-c", + f"transport select {transport}", + "-f", + target or os.environ.get("OPENOCD_TARGET", DEFAULT_TARGET), + "-c", + ( + f"if {{[lsearch -exact [target names] {DEFAULT_CPU_TARGET}] >= 0}} " + f"{{ {DEFAULT_CPU_TARGET} configure -defer-examine }}" + ), + "-c", + f"adapter speed {speed}", + ] + if serial: + args += ["-c", f"adapter serial {serial}"] + return args + + +def runtime_gdbserver_cmd( + *, + openocd="openocd", + port=3333, + swo_port=None, + swo_traceclk="100000000", + swo_pin_freq="1000000", + swo_formatter="0", + swo_target=None, + tpiu_target=None, + speed="8000", + serial="", + transport="swd", +): + """Return the OpenOCD command used as the runtime GDB server.""" + cmd = openocd_base_args( + openocd=openocd, + speed=speed, + serial=serial, + transport=transport, + ) + [ + "-c", + "reset_config srst_only srst_nogate", + "-c", + f"gdb_port {port}", + "-c", + "tcl_port disabled", + "-c", + "telnet_port disabled", + "-c", + "init", + "-c", + f"{DEFAULT_CPU_TARGET} arp_examine", + ] + if swo_port is not None: + swo_target = swo_target or swo_target_from_env() + tpiu_target = tpiu_target or tpiu_target_from_env() + if tpiu_target: + cmd += [ + "-c", + ( + f"{tpiu_target} configure -protocol uart " + f"-traceclk {swo_traceclk} -pin-freq {swo_pin_freq} " + f"-formatter {swo_formatter}" + ), + "-c", + f"{tpiu_target} enable", + ] + cmd += [ + "-c", + ( + f"{swo_target} configure -protocol uart " + f"-traceclk {swo_traceclk} -pin-freq {swo_pin_freq} " + f"-output :{swo_port} -formatter {swo_formatter}" + ), + "-c", + f"{swo_target} enable", + "-c", + f"{DEFAULT_CPU_TARGET} itm port 0 on", + ] + return cmd + + +def flexmem_script_lines( + *, + timeout_ms, + mem_target=DEFAULT_CPU_TARGET, + rcc_apb4ensr2_addr="0x56028a78", + cm55tcmcr_addr="0x56008008", + cm55rstcr_addr="0x56008018", + expected_mask=0xFF, + expected_value=0x99, +): + """Return an OpenOCD TCL script for configuring STM32N6 FLEXMEM.""" + return [ + "reset_config none", + "init", + f"{mem_target} arp_examine", + "proc read32 {addr} {", + f" return [lindex [{mem_target} read_memory $addr 32 1] 0]", + "}", + "proc write32 {addr value} {", + f" {mem_target} mww $addr $value", + "}", + f"set rcc_apb4ensr2 [read32 {rcc_apb4ensr2_addr}]", + f"write32 {rcc_apb4ensr2_addr} [expr {{$rcc_apb4ensr2 | 0x1}}]", + f"set cm55tcmcr [read32 {cm55tcmcr_addr}]", + f"write32 {cm55tcmcr_addr} [expr {{($cm55tcmcr & ~0xff) | 0x{expected_value:x}}}]", + f"set cm55rstcr [read32 {cm55rstcr_addr}]", + f"write32 {cm55rstcr_addr} [expr {{$cm55rstcr | 0x1}}]", + "proc wait_flexmem_configured {} {", + f" set deadline [expr {{[clock milliseconds] + {int(timeout_ms)}}}]", + " while {[clock milliseconds] < $deadline} {", + f" set value [read32 {cm55tcmcr_addr}]", + f" if {{($value & 0x{expected_mask:x}) == 0x{expected_value:x}}} {{ return }}", + " sleep 200", + " }", + f' error "FLEXMEM configuration register did not reach expected 0x{expected_value:x} value"', + "}", + "wait_flexmem_configured", + "reset_config none", + "reset run", + "shutdown", + ] diff --git a/test/zephyr/nucleo_n657x0_q/nucleo_host/results.py b/test/zephyr/nucleo_n657x0_q/nucleo_host/results.py new file mode 100644 index 0000000000..e534e74736 --- /dev/null +++ b/test/zephyr/nucleo_n657x0_q/nucleo_host/results.py @@ -0,0 +1,107 @@ +# Copyright (c) The mldsa-native project authors +# Copyright (c) The mlkem-native project authors +# Copyright (c) Arm Ltd. +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +"""Parse GDB failure output and Cortex-M fault diagnostics.""" + +import re + +HARDFAULT_SENTINEL = "[[NUCLEO-HARDFAULT]]" + + +def decode_cfsr(cfsr: int): + """Return names of set Configurable Fault Status Register bits.""" + bits = [ + (0, "IACCVIOL"), + (1, "DACCVIOL"), + (3, "MUNSTKERR"), + (4, "MSTKERR"), + (5, "MLSPERR"), + (7, "MMARVALID"), + (8, "IBUSERR"), + (9, "PRECISERR"), + (10, "IMPRECISERR"), + (11, "UNSTKERR"), + (12, "STKERR"), + (13, "LSPERR"), + (15, "BFARVALID"), + (16, "UNDEFINSTR"), + (17, "INVSTATE"), + (18, "INVPC"), + (19, "NOCP"), + (24, "UNALIGNED"), + (25, "DIVBYZERO"), + ] + return [name for bit, name in bits if cfsr & (1 << bit)] + + +def decode_hfsr(hfsr: int): + """Return names of set HardFault Status Register bits.""" + bits = [(1, "VECTTBL"), (30, "FORCED"), (31, "DEBUGEVT")] + return [name for bit, name in bits if hfsr & (1 << bit)] + + +def fault_info_from_gdb(gdb_text: str) -> str: + """Format fault registers emitted by the GDB script into readable text.""" + values = {} + register_pattern = ( + r"^(CFSR|HFSR|DFSR|MMFAR|BFAR|AFSR|SHCSR|CCR|MSP|PSP|LR|PC)" + r"=0x([0-9a-fA-F]+)$" + ) + for name, value in re.findall(register_pattern, gdb_text, re.MULTILINE): + values[name] = int(value, 16) + + if not values: + return "" + + lines = ["Fault registers:"] + for name in ( + "CFSR", + "HFSR", + "DFSR", + "MMFAR", + "BFAR", + "AFSR", + "SHCSR", + "CCR", + "MSP", + "PSP", + "LR", + "PC", + ): + if name in values: + lines.append(f" {name}=0x{values[name]:08x}") + + cfsr_bits = decode_cfsr(values.get("CFSR", 0)) + hfsr_bits = decode_hfsr(values.get("HFSR", 0)) + if cfsr_bits: + lines.append(" CFSR bits: " + ", ".join(cfsr_bits)) + if hfsr_bits: + lines.append(" HFSR bits: " + ", ".join(hfsr_bits)) + + # The stack dump follows a marker printed by the GDB script. Keep parsing + # permissive because GDB may format the memory rows differently by version. + stacked = re.search( + r"^STACKED_R0_R1_R2_R3_R12_LR_PC_XPSR:\s*\n" + r"((?:0x[0-9a-fA-F]+:\s+.*\n?)?)", + gdb_text, + re.MULTILINE, + ) + if stacked: + stack_lines = [ + line.strip() for line in stacked.group(1).splitlines() if line.strip() + ] + if stack_lines: + lines.append(" stacked frame dump:") + lines.extend(f" {line}" for line in stack_lines) + + return "\n".join(lines) + + +def gdb_observed_hardfault(gdb_text: str) -> bool: + """Return whether GDB output shows the target entered HardFault_Handler.""" + return ( + HARDFAULT_SENTINEL in gdb_text + or re.search(r"^HardFault_Handler \(\)", gdb_text, re.MULTILINE) is not None + ) diff --git a/test/zephyr/nucleo_n657x0_q/nucleo_host/symbols.py b/test/zephyr/nucleo_n657x0_q/nucleo_host/symbols.py new file mode 100644 index 0000000000..4951b35763 --- /dev/null +++ b/test/zephyr/nucleo_n657x0_q/nucleo_host/symbols.py @@ -0,0 +1,79 @@ +# Copyright (c) The mldsa-native project authors +# Copyright (c) The mlkem-native project authors +# Copyright (c) Arm Ltd. +# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT + +"""Resolve symbols from ARM ELF files using ``nm`` and ``readelf`` output.""" + +import shutil +import subprocess + + +def default_readelf(): + """Return the preferred readelf executable name available on this host.""" + return shutil.which("arm-none-eabi-readelf") or shutil.which("readelf") or "readelf" + + +def resolve_symbol(elf_path: str, symbol: str, nm="arm-none-eabi-nm", readelf=None): + """Resolve ``symbol`` to a hex address.""" + addr = resolve_symbol_with_nm(elf_path, symbol, nm) + if addr is not None: + return addr + return resolve_symbol_with_readelf(elf_path, symbol, readelf or default_readelf()) + + +def resolve_symbol_with_nm(elf_path: str, symbol: str, nm="arm-none-eabi-nm"): + """Resolve ``symbol`` with ``nm -n`` and return ``None`` on any failure.""" + try: + cp = subprocess.run( + [nm, "-n", elf_path], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + except OSError: + return None + if cp.returncode != 0: + return None + return parse_nm_symbol(cp.stdout, symbol) + + +def parse_nm_symbol(output: str, symbol: str): + """Parse one symbol address from ``nm -n`` output.""" + for line in output.splitlines(): + parts = line.strip().split() + if len(parts) >= 3 and parts[-1] == symbol: + addr_hex = parts[0] + if not addr_hex.startswith("0x"): + addr_hex = "0x" + addr_hex + return addr_hex + return None + + +def resolve_symbol_with_readelf(elf_path: str, symbol: str, readelf=None): + """Resolve ``symbol`` with ``readelf -s``.""" + try: + cp = subprocess.run( + [readelf or default_readelf(), "-s", elf_path], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + except OSError: + return None + if cp.returncode != 0: + return None + return parse_readelf_symbol(cp.stdout, symbol) + + +def parse_readelf_symbol(output: str, symbol: str): + """Parse one symbol address from ``readelf -s`` output.""" + for line in output.splitlines(): + if symbol not in line: + continue + fields = line.split() + if len(fields) >= 8 and fields[-1] == symbol: + val = fields[1] + if all(char in "0123456789abcdefABCDEF" for char in val): + return "0x" + val + return None diff --git a/test/zephyr/platform.mk b/test/zephyr/platform.mk index 8656e68ddc..03b863ff8c 100644 --- a/test/zephyr/platform.mk +++ b/test/zephyr/platform.mk @@ -16,8 +16,9 @@ PLATFORM_PATH := test/zephyr # define it here too so CUSTOM_BUILD below expands to the right path. BUILD_DIR ?= test/build -# ZEPHYR_TARGET= selects a target. Each key maps to a Zephyr board and the -# QEMU machine emulating it; add a board with a row below. +# ZEPHYR_TARGET= selects a target. QEMU targets map to both a Zephyr board +# and the QEMU machine emulating it; hardware targets map to a Zephyr board and +# provide their own execution wrapper. Add a target with a row below. ZEPHYR_TARGET ?= mps3-an547 ZEPHYR_BOARD_mps2-an385 := mps2/an385 @@ -30,13 +31,21 @@ ZEPHYR_BOARD_mps2-an521 := mps2/an521/cpu0 ZEPHYR_QEMU_mps2-an521 := mps2-an521 # Cortex-M33 ZEPHYR_BOARD_mps3-an547 := mps3/corstone300/an547 ZEPHYR_QEMU_mps3-an547 := mps3-an547 # Cortex-M55 +ZEPHYR_BOARD_nucleo-n657x0-q := nucleo_n657x0_q # Cortex-M55 (hardware) ZEPHYR_FIPS202_BACKEND_mps3-an547 := fips202/native/armv81m/mve.h +ZEPHYR_FIPS202_BACKEND_nucleo-n657x0-q := fips202/native/armv81m/mve.h -ZEPHYR_TARGETS := mps2-an385 mps2-an386 mps2-an500 mps2-an521 mps3-an547 +ZEPHYR_TARGETS := mps2-an385 mps2-an386 mps2-an500 mps2-an521 mps3-an547 nucleo-n657x0-q ZEPHYR_BOARD := $(ZEPHYR_BOARD_$(ZEPHYR_TARGET)) export QEMU_MACHINE := $(strip $(ZEPHYR_QEMU_$(ZEPHYR_TARGET))) +ZEPHYR_IS_NUCLEO_N657X0_Q := $(filter nucleo-n657x0-q,$(ZEPHYR_TARGET)) + +ifneq ($(ZEPHYR_IS_NUCLEO_N657X0_Q),) +CROSS_PREFIX ?= arm-none-eabi- +CC = gcc +endif ifeq ($(ZEPHYR_BOARD),) $(error Unknown ZEPHYR_TARGET '$(ZEPHYR_TARGET)'. Supported: $(ZEPHYR_TARGETS)) @@ -51,6 +60,34 @@ ZEPHYR_FIPS202_BACKEND := $(if $(filter 1,$(OPT)),$(strip $(ZEPHYR_FIPS202_BACKE ZEPHYR_APP := $(PLATFORM_PATH)/app ZEPHYR_BUILD_DIR := $(BUILD_DIR)/zephyr/$(ZEPHYR_TARGET) +ZEPHYR_ACTIVE_TARGET := $(BUILD_DIR)/zephyr/.active-target +ZEPHYR_APP_INPUTS := \ + $(ZEPHYR_APP)/CMakeLists.txt \ + $(ZEPHYR_APP)/Kconfig \ + $(ZEPHYR_APP)/prj.conf \ + $(ZEPHYR_APP)/nucleo_n657x0_q.conf \ + $(ZEPHYR_APP)/shim.c \ + $(ZEPHYR_APP)/shim_nucleo_n657x0_q.c \ + $(ZEPHYR_APP)/nucleo_n657x0_q.overlay +ZEPHYR_NUCLEO_PLATFORM_PATH := $(PLATFORM_PATH)/nucleo_n657x0_q +ZEPHYR_NUCLEO_OVERLAY := $(abspath $(ZEPHYR_APP)/nucleo_n657x0_q.overlay) +ZEPHYR_NUCLEO_CONF := $(abspath $(ZEPHYR_APP)/nucleo_n657x0_q.conf) +ZEPHYR_TARGET_CMAKE_ARGS := $(if $(ZEPHYR_IS_NUCLEO_N657X0_Q),\ + -DZEPHYR_NUCLEO_N657X0_Q=ON \ + -DEXTRA_CONF_FILE=$(ZEPHYR_NUCLEO_CONF) \ + -DDTC_OVERLAY_FILE=$(ZEPHYR_NUCLEO_OVERLAY)) + +# Test binary output paths are shared across ZEPHYR_TARGET values, while the +# CMake build directory is target-specific. Keep a lightweight marker containing +# the last requested target, and only touch it when the target changes. Binaries +# depending on this marker are then rebuilt after a target switch without +# forcing a clean rebuild when the target is unchanged. +.PHONY: zephyr_target_marker_force +$(ZEPHYR_ACTIVE_TARGET): zephyr_target_marker_force + $(Q)[ -d $(@D) ] || mkdir -p $(@D) + $(Q)if [ ! -f $@ ] || [ "$$(cat $@)" != "$(ZEPHYR_TARGET)" ]; then \ + echo "$(ZEPHYR_TARGET)" > $@; \ + fi # Per-binary CMake build dir, keyed on $(notdir $@) so binaries build in # parallel. Recipe-expanded, so $@ is the specific bin being built. @@ -78,19 +115,34 @@ CFLAGS += -DNTESTS=3 \ # Requires AUTO=0 (see .github/workflows/zephyr.yml): the host-arch flags AUTO=1 # adds must not reach the Zephyr toolchain, which selects the target arch itself. ZEPHYR_TEST_CFLAGS = $(subst \",\\\",$(patsubst -Imldsa,-I$(abspath mldsa),$(CFLAGS))) +# Keep make-exported project flags out of Zephyr's own CMake build; the app +# sources get those flags explicitly via ZEPHYR_TEST_CFLAGS. +ZEPHYR_CMAKE_ENV := env -u CFLAGS -u CXXFLAGS -u CPPFLAGS -u LDFLAGS CUSTOM_BUILD = \ echo " ZEPHYR $(ZEPHYR_TARGET): $(notdir $@)" && \ - cmake -GNinja -S $(ZEPHYR_APP) -B $(ZEPHYR_OUT) \ + $(ZEPHYR_CMAKE_ENV) cmake -GNinja -S $(ZEPHYR_APP) -B $(ZEPHYR_OUT) \ -DBOARD=$(ZEPHYR_BOARD) \ -DZEPHYR_NATIVE_ROOT=$(CURDIR) \ -DZEPHYR_TEST_SRCS="$(strip $(TEST_SRCS))" \ -DZEPHYR_TEST_CFLAGS="$(ZEPHYR_TEST_CFLAGS)" \ -DZEPHYR_FIPS202_BACKEND=$(ZEPHYR_FIPS202_BACKEND) \ $(if $(ZEPHYR_FIPS202_BACKEND),-DCONFIG_FIPS202_MVE_BACKEND=y) \ + $(ZEPHYR_TARGET_CMAKE_ARGS) \ -DUSER_CACHE_DIR=$(abspath $(ZEPHYR_OUT)/.cache) \ >/dev/null && \ - cmake --build $(ZEPHYR_OUT) >/dev/null && \ + $(ZEPHYR_CMAKE_ENV) cmake --build $(ZEPHYR_OUT) >/dev/null && \ cp $(ZEPHYR_OUT)/zephyr/zephyr.elf $@ +# A custom build links the test sources directly rather than from objects, so +# nothing otherwise makes the bins depend on the Zephyr app inputs or the +# active-target marker. components.mk attaches CUSTOM_BUILD_DEPS to every test +# binary (in its CUSTOM_BUILD branch), so a CMakeLists/shim/overlay edit or a +# target switch forces a rebuild. Set here (before components.mk is included). +CUSTOM_BUILD_DEPS := $(ZEPHYR_ACTIVE_TARGET) $(ZEPHYR_APP_INPUTS) + +ifeq ($(ZEPHYR_IS_NUCLEO_N657X0_Q),) EXEC_WRAPPER := $(abspath $(PLATFORM_PATH)/exec_wrapper.py) +else +EXEC_WRAPPER := $(abspath $(ZEPHYR_NUCLEO_PLATFORM_PATH)/exec_wrapper.py) +endif