From fbaf869fb9836e98b5ca5cfbf8c3d64812898478 Mon Sep 17 00:00:00 2001 From: Alexey Lapshin Date: Wed, 17 Jun 2026 15:04:56 +0700 Subject: [PATCH] feat(build): add RISC-V ZCMP post-link workaround check Validate linked RISC-V executables when CONFIG_COMPILER_ENABLE_RISCV_ZCMP is enabled on affected chips. Disassemble each function and reject mstatus.MIE clears that lack an earlier mintthresh write of 0xff. Handle csrrci, csrrw, and register-mask csrrc patterns while ignoring csrrs. Add build-only coverage for valid and invalid sequences with CMake v1 and v2. Stop the hardware stack guard before switching stacks during restart, and keep ZCMP disabled for TEE test apps that still require the workaround. --- .../test_apps/tee/main/CMakeLists.txt | 8 - .../test_apps/tee/sdkconfig.defaults.esp32c5 | 3 + .../test_apps/tee/sdkconfig.defaults.esp32c61 | 3 + .../lowpower/port/esp32c61/sleep_cpu.c | 14 -- .../port/soc/esp32c5/system_internal.c | 9 + .../port/soc/esp32c61/system_internal.c | 9 + .../port/soc/esp32h4/system_internal.c | 9 + .../port/soc/esp32p4/system_internal.c | 9 + components/riscv/CMakeLists.txt | 34 ++++ components/riscv/check_zcmp_workaround.cmake | 179 ++++++++++++++++++ components/riscv/hints.yml | 3 + .../riscv/test_apps/.build-test-rules.yml | 13 ++ .../CMakeLists.txt | 7 + .../test_zcmp_workaround_checking/README.md | 11 ++ .../main/CMakeLists.txt | 5 + .../main/test_zcmp_workaround_checking.c | 24 +++ .../sdkconfig.defaults | 1 + .../buildv2/test_zcmp_workaround.py | 34 ++++ .../test_build_system/test_zcmp_workaround.py | 23 +++ 19 files changed, 376 insertions(+), 22 deletions(-) create mode 100644 components/riscv/check_zcmp_workaround.cmake create mode 100644 components/riscv/hints.yml create mode 100644 components/riscv/test_apps/.build-test-rules.yml create mode 100644 components/riscv/test_apps/test_zcmp_workaround_checking/CMakeLists.txt create mode 100644 components/riscv/test_apps/test_zcmp_workaround_checking/README.md create mode 100644 components/riscv/test_apps/test_zcmp_workaround_checking/main/CMakeLists.txt create mode 100644 components/riscv/test_apps/test_zcmp_workaround_checking/main/test_zcmp_workaround_checking.c create mode 100644 components/riscv/test_apps/test_zcmp_workaround_checking/sdkconfig.defaults create mode 100644 tools/test_build_system/buildv2/test_zcmp_workaround.py create mode 100644 tools/test_build_system/test_zcmp_workaround.py diff --git a/components/esp_hal_security/test_apps/tee/main/CMakeLists.txt b/components/esp_hal_security/test_apps/tee/main/CMakeLists.txt index a73b1b83003..9feb9c83b4b 100644 --- a/components/esp_hal_security/test_apps/tee/main/CMakeLists.txt +++ b/components/esp_hal_security/test_apps/tee/main/CMakeLists.txt @@ -7,11 +7,3 @@ idf_component_register(SRCS "${srcs}" INCLUDE_DIRS "" PRIV_REQUIRES pms_and_cpu_intr esp_psram esp_hal_security WHOLE_ARCHIVE) - -# TODO: IDF-14145 -if(CONFIG_IDF_TARGET_ESP32C5 OR CONFIG_IDF_TARGET_ESP32C61) - # Disable zcmp extension to avoid hardware issue with interrupts (DIG-661) - set(march_option "-march=rv32imac_zicsr_zifencei_zaamo_zalrsc") - idf_build_set_property(COMPILE_OPTIONS "${march_option}" APPEND) - idf_build_set_property(LINK_OPTIONS "${march_option}" APPEND) -endif() diff --git a/components/esp_hal_security/test_apps/tee/sdkconfig.defaults.esp32c5 b/components/esp_hal_security/test_apps/tee/sdkconfig.defaults.esp32c5 index 0139d521ec0..d87ea7b932e 100644 --- a/components/esp_hal_security/test_apps/tee/sdkconfig.defaults.esp32c5 +++ b/components/esp_hal_security/test_apps/tee/sdkconfig.defaults.esp32c5 @@ -5,3 +5,6 @@ CONFIG_ESP_SYSTEM_ALLOW_RTC_FAST_MEM_AS_HEAP=n # Enable SPIRAM CONFIG_SPIRAM=y + +# TODO: IDF-14145 +CONFIG_COMPILER_ENABLE_RISCV_ZCMP=n diff --git a/components/esp_hal_security/test_apps/tee/sdkconfig.defaults.esp32c61 b/components/esp_hal_security/test_apps/tee/sdkconfig.defaults.esp32c61 index 5665cfbb5cd..3125c3a97af 100644 --- a/components/esp_hal_security/test_apps/tee/sdkconfig.defaults.esp32c61 +++ b/components/esp_hal_security/test_apps/tee/sdkconfig.defaults.esp32c61 @@ -1,2 +1,5 @@ # Enable SPIRAM CONFIG_SPIRAM=y + +# TODO: IDF-14145 +CONFIG_COMPILER_ENABLE_RISCV_ZCMP=n diff --git a/components/esp_hw_support/lowpower/port/esp32c61/sleep_cpu.c b/components/esp_hw_support/lowpower/port/esp32c61/sleep_cpu.c index 350e726a7c9..71b2596c796 100644 --- a/components/esp_hw_support/lowpower/port/esp32c61/sleep_cpu.c +++ b/components/esp_hw_support/lowpower/port/esp32c61/sleep_cpu.c @@ -70,20 +70,6 @@ FORCE_INLINE_ATTR void restore_csr_enable_global_int(uint32_t mstatus_val, uint3 #endif } -#if __riscv_zcmp && SOC_CPU_ZCMP_WORKAROUND -FORCE_INLINE_ATTR uint32_t save_mintthresh_and_disable_global_int(void) -{ - /* Due to the reason described in IDF-14279, when mie is set to 0, mintthresh needs to be set to 0xff. */ - // TODO: IDF-14279 DIG-661 - return RV_READ_MINTTHRESH_AND_DISABLE_INTR(); -} - -FORCE_INLINE_ATTR void restore_mintthresh(uint32_t mintthresh_val) -{ - RV_RESTORE_MINTTHRESH(mintthresh_val); -} -#endif - static IRAM_ATTR RvCoreNonCriticalSleepFrame * rv_core_noncritical_regs_save(void) { assert(s_cpu_retention.retent.non_critical_frame); diff --git a/components/esp_system/port/soc/esp32c5/system_internal.c b/components/esp_system/port/soc/esp32c5/system_internal.c index ea641db79f1..d9406b1c5cf 100644 --- a/components/esp_system/port/soc/esp32c5/system_internal.c +++ b/components/esp_system/port/soc/esp32c5/system_internal.c @@ -28,6 +28,9 @@ #endif #include "esp_private/cache_err_int.h" #include "esp_memory_utils.h" +#if CONFIG_ESP_SYSTEM_HW_STACK_GUARD && CONFIG_COMPILER_ENABLE_RISCV_ZCMP +#include "esp_private/hw_stack_guard.h" +#endif #define ALIGN_DOWN(val, align) ((val) & ~((align) - 1)) extern int _bss_end; @@ -163,6 +166,12 @@ void esp_restart_noos(void) // If stack is in external RAM (CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM), switch SP to // internal RAM before disabling the cache to avoid a "Cache disabled but cached memory // region accessed" crash. +#if CONFIG_ESP_SYSTEM_HW_STACK_GUARD && CONFIG_COMPILER_ENABLE_RISCV_ZCMP + // Stop hw stack guard before changing SP: new SP is outside task stack bounds. + // rtc_clk_cpu_set_to_default_config() (called later) uses vPortEnterCritical() via + // ENABLE_CLK_GATE, which can trigger DIG-661 (assist-debug interrupt with mstatus.mie=0). + esp_hw_stack_guard_monitor_stop(); +#endif uint32_t new_sp = ALIGN_DOWN((uint32_t)&_bss_end, 16); rv_utils_set_sp((void *)new_sp); } diff --git a/components/esp_system/port/soc/esp32c61/system_internal.c b/components/esp_system/port/soc/esp32c61/system_internal.c index 2fe762e97a0..19495ab3235 100644 --- a/components/esp_system/port/soc/esp32c61/system_internal.c +++ b/components/esp_system/port/soc/esp32c61/system_internal.c @@ -24,6 +24,9 @@ #include "hal/uart_ll.h" #include "esp_private/cache_err_int.h" #include "esp_memory_utils.h" +#if CONFIG_ESP_SYSTEM_HW_STACK_GUARD && CONFIG_COMPILER_ENABLE_RISCV_ZCMP +#include "esp_private/hw_stack_guard.h" +#endif #define ALIGN_DOWN(val, align) ((val) & ~((align) - 1)) extern int _bss_end; @@ -153,6 +156,12 @@ void esp_restart_noos(void) // If stack is in external RAM (CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM), switch SP to // internal RAM before disabling the cache to avoid a "Cache disabled but cached memory // region accessed" crash. +#if CONFIG_ESP_SYSTEM_HW_STACK_GUARD && CONFIG_COMPILER_ENABLE_RISCV_ZCMP + // Stop hw stack guard before changing SP: new SP is outside task stack bounds. + // rtc_clk_cpu_set_to_default_config() (called later) uses vPortEnterCritical() via + // ENABLE_CLK_GATE, which can trigger DIG-661 (assist-debug interrupt with mstatus.mie=0). + esp_hw_stack_guard_monitor_stop(); +#endif uint32_t new_sp = ALIGN_DOWN((uint32_t)&_bss_end, 16); rv_utils_set_sp((void *)new_sp); } diff --git a/components/esp_system/port/soc/esp32h4/system_internal.c b/components/esp_system/port/soc/esp32h4/system_internal.c index a6e1080d694..d5b0a082735 100644 --- a/components/esp_system/port/soc/esp32h4/system_internal.c +++ b/components/esp_system/port/soc/esp32h4/system_internal.c @@ -23,6 +23,9 @@ #include "hal/wdt_hal.h" #include "hal/uart_ll.h" #include "esp_memory_utils.h" +#if CONFIG_ESP_SYSTEM_HW_STACK_GUARD && CONFIG_COMPILER_ENABLE_RISCV_ZCMP +#include "esp_private/hw_stack_guard.h" +#endif #define ALIGN_DOWN(val, align) ((val) & ~((align) - 1)) extern int _bss_end; @@ -170,6 +173,12 @@ void esp_restart_noos(void) // If stack is in external RAM (CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM), switch SP to // internal RAM before disabling the cache to avoid a "Cache disabled but cached memory // region accessed" crash. +#if CONFIG_ESP_SYSTEM_HW_STACK_GUARD && CONFIG_COMPILER_ENABLE_RISCV_ZCMP + // Stop hw stack guard before changing SP: new SP is outside task stack bounds. + // rtc_clk_cpu_set_to_default_config() (called later) uses vPortEnterCritical() via + // ENABLE_CLK_GATE, which can trigger DIG-661 (assist-debug interrupt with mstatus.mie=0). + esp_hw_stack_guard_monitor_stop(); +#endif uint32_t new_sp = ALIGN_DOWN((uint32_t)&_bss_end, 16); rv_utils_set_sp((void *)new_sp); } diff --git a/components/esp_system/port/soc/esp32p4/system_internal.c b/components/esp_system/port/soc/esp32p4/system_internal.c index 9b3c089b1e4..9a6a1950529 100644 --- a/components/esp_system/port/soc/esp32p4/system_internal.c +++ b/components/esp_system/port/soc/esp32p4/system_internal.c @@ -34,6 +34,9 @@ #include "hal/dma2d_ll.h" #include "hal/efuse_hal.h" #include "esp_memory_utils.h" +#if CONFIG_ESP_SYSTEM_HW_STACK_GUARD && CONFIG_COMPILER_ENABLE_RISCV_ZCMP +#include "esp_private/hw_stack_guard.h" +#endif #define ALIGN_DOWN(val, align) ((val) & ~((align) - 1)) extern int _bss_end; @@ -249,6 +252,12 @@ void esp_restart_noos(void) // If stack is in external RAM (CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM), switch SP to // internal RAM before disabling the cache to avoid a "Cache disabled but cached memory // region accessed" crash. +#if CONFIG_ESP_SYSTEM_HW_STACK_GUARD && CONFIG_COMPILER_ENABLE_RISCV_ZCMP + // Stop hw stack guard before changing SP: new SP is outside task stack bounds. + // rtc_clk_cpu_set_to_default_config() (called later) uses vPortEnterCritical() via + // ENABLE_CLK_GATE, which can trigger DIG-661 (assist-debug interrupt with mstatus.mie=0). + esp_hw_stack_guard_monitor_stop(); +#endif uint32_t new_sp = ALIGN_DOWN((uint32_t)&_bss_end, 16); rv_utils_set_sp((void *)new_sp); } diff --git a/components/riscv/CMakeLists.txt b/components/riscv/CMakeLists.txt index 12872530fe6..2f62cbe16a2 100644 --- a/components/riscv/CMakeLists.txt +++ b/components/riscv/CMakeLists.txt @@ -41,3 +41,37 @@ idf_component_register(SRCS "${srcs}" PRIV_REQUIRES ${priv_requires}) target_linker_script(${COMPONENT_LIB} INTERFACE "ld/rom.api.ld") + +# TODO: IDF-14145 +if(CONFIG_COMPILER_ENABLE_RISCV_ZCMP AND + NOT CONFIG_ESP32P4_SELECTS_REV_LESS_V3 AND + NOT CONFIG_SECURE_ENABLE_TEE AND + NOT ESP_TEE_BUILD) + if(IDF_BUILD_V2) + function(riscv_check_zcmp_workaround target) + add_custom_command(TARGET ${target} POST_BUILD + COMMAND ${CMAKE_COMMAND} + -DELF_FILE=$ + -DCMAKE_OBJDUMP=${CMAKE_OBJDUMP} + -P "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/check_zcmp_workaround.cmake" + COMMENT "Checking ZCMP workaround in linked executable" + VERBATIM) + endfunction() + idf_component_register_build_event_callback(EVENT POST_ELF CALLBACK riscv_check_zcmp_workaround) + else() + idf_build_get_property(build_dir BUILD_DIR) + idf_build_get_property(elf_target EXECUTABLE GENERATOR_EXPRESSION) + set(_riscv_zcmp_check_marker "${build_dir}/.zcmp_workaround_checked") + add_custom_command(OUTPUT "${_riscv_zcmp_check_marker}" + COMMAND ${CMAKE_COMMAND} + -DELF_FILE=$> + -DCMAKE_OBJDUMP=${CMAKE_OBJDUMP} + -P "${CMAKE_CURRENT_LIST_DIR}/check_zcmp_workaround.cmake" + COMMAND ${CMAKE_COMMAND} -E touch "${_riscv_zcmp_check_marker}" + DEPENDS "$>" + COMMENT "Checking ZCMP workaround in linked executable" + VERBATIM) + add_custom_target(riscv_check_zcmp_workaround DEPENDS "${_riscv_zcmp_check_marker}") + idf_build_add_post_elf_dependency("${CMAKE_PROJECT_NAME}.elf" riscv_check_zcmp_workaround) + endif() +endif() diff --git a/components/riscv/check_zcmp_workaround.cmake b/components/riscv/check_zcmp_workaround.cmake new file mode 100644 index 00000000000..6e39a0495d7 --- /dev/null +++ b/components/riscv/check_zcmp_workaround.cmake @@ -0,0 +1,179 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 +# +# Post-link check for RISC-V ZCMP workaround compliance. +# +# Required variables: +# ELF_FILE - path to the linked executable +# CMAKE_OBJDUMP - objdump tool from the toolchain +# +# Pure CMake implementation: objdump output is written to a temporary .objdump file, +# scanned line-by-line with file(STRINGS), then removed. Empty objdump lines are skipped. +# +# For every instruction that clears mstatus.MIE (bit 3) this check verifies that a +# mintthresh write (csrrw ...,0x347,...) of 0xff appears earlier in the same function. +# Instructions considered to clear MIE: +# csrrci mstatus,8 - immediate clear of bit 3 +# csrrw ...,mstatus,... - full mstatus overwrite +# csrrc ...,mstatus,... - register clear, only when the mask loaded +# on the previous line has bit 3 set +# csrrs (bit set) is intentionally ignored: it cannot clear MIE. + +set(_zcmp_disasm_file "${ELF_FILE}.objdump") +set(_mstatus_mie_re "csrrci.*mstatus,8|csrrw.*mstatus") +set(_mstatus_csrrc_re "csrrc.*mstatus") +set(_mintthresh_csrrw_re "csrrw.*,0x347,") +set(_mintthresh_value_base_re "(255|-256|0xff)") +set(_mintthresh_stack_move_re "(^|[ \t])(c\\.sw|c\\.lw|sw|lw)") +set(_mintthresh_call_re "(^|[ \t])(c\\.jal|jal)") +set(_function_start_re "^[0-9a-fA-F]+ <([^>]+)>:$") + +# Extract the last comma-separated operand of a disassembly line (e.g. "li a5,8" -> "8"). +function(_zcmp_last_operand line out_var) + set(${out_var} "" PARENT_SCOPE) + if(line MATCHES ",([^, \t]+)[ \t]*$") + set(${out_var} "${CMAKE_MATCH_1}" PARENT_SCOPE) + endif() +endfunction() + +# TRUE when the numeric operand (decimal or hex) has bit 3 set, i.e. it would clear MIE. +function(_zcmp_operand_clears_mie operand out_var) + set(_value 0) + if(operand MATCHES "^-?(0[xX][0-9a-fA-F]+|[0-9]+)$") + string(TOLOWER "${operand}" _norm) + math(EXPR _value "${_norm}") + endif() + math(EXPR _bit3 "${_value} & 0x8") + if(_bit3 EQUAL 0) + set(${out_var} FALSE PARENT_SCOPE) + else() + set(${out_var} TRUE PARENT_SCOPE) + endif() +endfunction() + +if(NOT DEFINED ELF_FILE) + message(FATAL_ERROR "ELF_FILE is not set") +endif() + +if(NOT DEFINED CMAKE_OBJDUMP) + message(FATAL_ERROR "CMAKE_OBJDUMP is not set") +endif() + +if(NOT EXISTS "${ELF_FILE}") + message(FATAL_ERROR "ELF file not found: ${ELF_FILE}") +endif() + +execute_process( + COMMAND "${CMAKE_OBJDUMP}" -d -M no-aliases "${ELF_FILE}" + OUTPUT_FILE "${_zcmp_disasm_file}" + ERROR_VARIABLE _zcmp_objdump_err + RESULT_VARIABLE _zcmp_objdump_result +) + +if(NOT _zcmp_objdump_result EQUAL 0) + message(FATAL_ERROR "Failed to disassemble ${ELF_FILE}: ${_zcmp_objdump_err}") +endif() + +file(STRINGS "${_zcmp_disasm_file}" _zcmp_lines) +file(REMOVE "${_zcmp_disasm_file}") + +set(_zcmp_function "") +set(_zcmp_function_lines "") +set(_zcmp_skip_function FALSE) + +# Functions exempt from the workaround (hand-audited / run with interrupts already masked). +set(_idf_trusted_function "rv_core_critical_regs_restore") + +foreach(_line IN LISTS _zcmp_lines) + string(STRIP "${_line}" _line) + if(_line STREQUAL "") + continue() + endif() + string(REPLACE ";" "\\;" _line "${_line}") + + # New function: reset the accumulated instruction window. The header line itself + # is not part of the window. + if(_line MATCHES "${_function_start_re}") + set(_zcmp_function "${CMAKE_MATCH_1}") + set(_zcmp_function_lines "") + # Trusted functions are exempt: skip every instruction until the next function header. + set(_zcmp_skip_function FALSE) + foreach(_trusted_function IN LISTS _idf_trusted_function) + if(_zcmp_function STREQUAL "${_trusted_function}") + set(_zcmp_skip_function TRUE) + break() + endif() + endforeach() + continue() + endif() + + if(_zcmp_skip_function) + continue() + endif() + + list(APPEND _zcmp_function_lines "${_line}") + list(LENGTH _zcmp_function_lines _line_count) + math(EXPR _cur_index "${_line_count} - 1") + + # ---- Does this line clear mstatus.MIE? ---- + set(_clears_mie FALSE) + if(_line MATCHES "${_mstatus_mie_re}") + set(_clears_mie TRUE) + elseif(_line MATCHES "${_mstatus_csrrc_re}") + set(_prev_operand "") + if(_cur_index GREATER 0) + math(EXPR _prev_index "${_cur_index} - 1") + list(GET _zcmp_function_lines ${_prev_index} _prev_line) + _zcmp_last_operand("${_prev_line}" _prev_operand) + endif() + _zcmp_operand_clears_mie("${_prev_operand}" _clears_mie) + endif() + + if(NOT _clears_mie) + continue() + endif() + + # ---- Require a preceding mintthresh write of 0xff in this function ---- + set(_has_mintthresh FALSE) + foreach(_scan_index RANGE 0 ${_cur_index}) + list(GET _zcmp_function_lines ${_scan_index} _scan_line) + if(NOT _scan_line MATCHES "${_mintthresh_csrrw_re}") + continue() + endif() + + # Found the mintthresh write. Extract its source register to narrow the value search. + _zcmp_last_operand("${_scan_line}" _mintthresh_src_reg) + if(_mintthresh_src_reg STREQUAL "") + continue() + endif() + set(_mintthresh_value_re "(^|[ \t])${_mintthresh_src_reg},(zero,)?${_mintthresh_value_base_re}") + + # Walk backward to the instruction that loaded 0xff into that register, + # skipping sw/lw stack traffic (-O0) and (c.)jal calls (-Os) the compiler may emit. + math(EXPR _value_index "${_scan_index} - 1") + while(_value_index GREATER_EQUAL 0) + list(GET _zcmp_function_lines ${_value_index} _value_line) + if(_value_line MATCHES "${_mintthresh_stack_move_re}" OR + _value_line MATCHES "${_mintthresh_call_re}") + math(EXPR _value_index "${_value_index} - 1") + continue() + endif() + if(NOT _value_line MATCHES "${_mintthresh_value_re}") + message(FATAL_ERROR + "ZCMP workaround violation: mintthresh write not preceded by value 0xff " + "(or equivalent 255/-256) in function ${_zcmp_function} in ${ELF_FILE}:\n${_value_line}") + endif() + set(_has_mintthresh TRUE) + break() + endwhile() + if(_has_mintthresh) + break() + endif() + endforeach() + + if(NOT _has_mintthresh) + message(FATAL_ERROR + "ZCMP workaround violation: mstatus access without mintthresh write " + "earlier in function ${_zcmp_function} in ${ELF_FILE}:\n${_line}") + endif() +endforeach() diff --git a/components/riscv/hints.yml b/components/riscv/hints.yml new file mode 100644 index 00000000000..14583514cce --- /dev/null +++ b/components/riscv/hints.yml @@ -0,0 +1,3 @@ +- + re: "ZCMP workaround violation" + hint: "Global interrupt disable (csrrc/csrrci mstatus,8) was found without the required mintthresh write (csrrw to CSR 0x347) within the previous instructions. This is unsafe with RISC-V ZCMP enabled on affected chips.\nEither apply the workaround before clearing mstatus.MIE (see SOC_CPU_ZCMP_WORKAROUND examples in ESP-IDF), or disable the ZCMP extension: set CONFIG_COMPILER_ENABLE_RISCV_ZCMP=n in sdkconfig / sdkconfig.defaults, or use 'idf.py menuconfig' -> 'Compiler options' -> disable 'Enable RISCV ZCMP extension'." diff --git a/components/riscv/test_apps/.build-test-rules.yml b/components/riscv/test_apps/.build-test-rules.yml new file mode 100644 index 00000000000..80f784ac3da --- /dev/null +++ b/components/riscv/test_apps/.build-test-rules.yml @@ -0,0 +1,13 @@ +# Documentation: .gitlab/ci/README.md#manifest-file-to-control-the-buildtest-apps + +components/riscv/test_apps/test_zcmp_workaround_checking: + enable: + - if: SOC_CPU_ZCMP_WORKAROUND == 1 + reason: builds chips that require the ZCMP mintthresh workaround + disable_test: + - if: SOC_CPU_ZCMP_WORKAROUND == 1 + reason: build-only check for ZCMP workaround + depends_components: + - riscv + - soc + - freertos diff --git a/components/riscv/test_apps/test_zcmp_workaround_checking/CMakeLists.txt b/components/riscv/test_apps/test_zcmp_workaround_checking/CMakeLists.txt new file mode 100644 index 00000000000..e66304815f4 --- /dev/null +++ b/components/riscv/test_apps/test_zcmp_workaround_checking/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 3.22) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +idf_build_set_property(MINIMAL_BUILD ON) + +project(test_zcmp_workaround_checking) diff --git a/components/riscv/test_apps/test_zcmp_workaround_checking/README.md b/components/riscv/test_apps/test_zcmp_workaround_checking/README.md new file mode 100644 index 00000000000..62d84838365 --- /dev/null +++ b/components/riscv/test_apps/test_zcmp_workaround_checking/README.md @@ -0,0 +1,11 @@ +| Supported Targets | ESP32-C5 | ESP32-C61 | ESP32-H4 | ESP32-P4 | +| ----------------- | -------- | --------- | -------- | -------- | + +Build-only check for the post-link ZCMP workaround validator. + +CI builds the default app on every target with ``SOC_CPU_ZCMP_WORKAROUND`` +(valid mintthresh workaround + ZCMP). No flash; a successful link is the pass +criterion. + +Negative coverage lives in ``tools/test_build_system`` (cmake v1/v2): build with +``-DTEST_INVALID_WORKAROUND=1`` must fail with ``ZCMP workaround violation``. diff --git a/components/riscv/test_apps/test_zcmp_workaround_checking/main/CMakeLists.txt b/components/riscv/test_apps/test_zcmp_workaround_checking/main/CMakeLists.txt new file mode 100644 index 00000000000..e05cf4c8790 --- /dev/null +++ b/components/riscv/test_apps/test_zcmp_workaround_checking/main/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register(SRCS "test_zcmp_workaround_checking.c") + +if(TEST_INVALID_WORKAROUND) + target_compile_definitions(${COMPONENT_LIB} PRIVATE TEST_INVALID_WORKAROUND) +endif() diff --git a/components/riscv/test_apps/test_zcmp_workaround_checking/main/test_zcmp_workaround_checking.c b/components/riscv/test_apps/test_zcmp_workaround_checking/main/test_zcmp_workaround_checking.c new file mode 100644 index 00000000000..cd76b795125 --- /dev/null +++ b/components/riscv/test_apps/test_zcmp_workaround_checking/main/test_zcmp_workaround_checking.c @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include + +void app_main(void) +{ + uint32_t mstatus; +#ifdef TEST_INVALID_WORKAROUND + /* Intentional ZCMP violation: clear MIE without mintthresh write. */ + __asm__ volatile("csrrci %0, mstatus, 0x8" : "=r"(mstatus)); +#else + __asm__ volatile( + "li t0, 0xff\n" + "csrrw zero, 0x347, t0\n" + "csrrci %0, mstatus, 0x8" + : "=r"(mstatus) + : + : "t0"); +#endif + (void)mstatus; +} diff --git a/components/riscv/test_apps/test_zcmp_workaround_checking/sdkconfig.defaults b/components/riscv/test_apps/test_zcmp_workaround_checking/sdkconfig.defaults new file mode 100644 index 00000000000..7d659180e76 --- /dev/null +++ b/components/riscv/test_apps/test_zcmp_workaround_checking/sdkconfig.defaults @@ -0,0 +1 @@ +CONFIG_COMPILER_ENABLE_RISCV_ZCMP=y diff --git a/tools/test_build_system/buildv2/test_zcmp_workaround.py b/tools/test_build_system/buildv2/test_zcmp_workaround.py new file mode 100644 index 00000000000..082a8e28f51 --- /dev/null +++ b/tools/test_build_system/buildv2/test_zcmp_workaround.py @@ -0,0 +1,34 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 +import logging +from pathlib import Path + +import pytest +from test_build_system_helpers import IdfPyFunc + +_IDF_TARGET = 'esp32c5' + +_CMAKEV2_CMAKELISTS = """\ +cmake_minimum_required(VERSION 3.22) + +include($ENV{IDF_PATH}/tools/cmakev2/idf.cmake) + +project(test_zcmp_workaround_checking C CXX ASM) + +idf_project_default() +""" + + +@pytest.mark.test_app_copy('components/riscv/test_apps/test_zcmp_workaround_checking') +@pytest.mark.usefixtures('test_app_copy') +def test_zcmp_workaround_checking(idf_py: IdfPyFunc, test_app_copy: Path) -> None: + """ZCMP POST_ELF check must fail when mstatus.mie is cleared without mintthresh workaround.""" + logging.info('Building zcmp workaround checking test app (build system v2)') + (test_app_copy / 'CMakeLists.txt').write_text(_CMAKEV2_CMAKELISTS, encoding='utf-8') + + idf_py('set-target', _IDF_TARGET) + ret = idf_py('-DTEST_INVALID_WORKAROUND=1', 'build', check=False) + output = (ret.stdout or '') + (ret.stderr or '') + + assert ret.returncode != 0, 'Build must fail when ZCMP workaround is violated' + assert 'ZCMP workaround violation' in output, 'Build output must report the ZCMP workaround violation' diff --git a/tools/test_build_system/test_zcmp_workaround.py b/tools/test_build_system/test_zcmp_workaround.py new file mode 100644 index 00000000000..ab8a59d3e0b --- /dev/null +++ b/tools/test_build_system/test_zcmp_workaround.py @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 +import logging + +import pytest +from test_build_system_helpers import IdfPyFunc + +_IDF_TARGET = 'esp32c5' + + +@pytest.mark.test_app_copy('components/riscv/test_apps/test_zcmp_workaround_checking') +@pytest.mark.usefixtures('test_app_copy') +@pytest.mark.buildv2_skip('Uses build system v1 project.cmake test app') +def test_zcmp_workaround_checking(idf_py: IdfPyFunc) -> None: + """ZCMP post-ELF check must fail when mstatus.mie is cleared without mintthresh workaround.""" + logging.info('Building zcmp workaround checking test app') + + idf_py('set-target', _IDF_TARGET) + ret = idf_py('-DTEST_INVALID_WORKAROUND=1', 'build', check=False) + output = (ret.stdout or '') + (ret.stderr or '') + + assert ret.returncode != 0, 'Build must fail when ZCMP workaround is violated' + assert 'ZCMP workaround violation' in output, 'Build output must report the ZCMP workaround violation'