Merge branch 'feature/use_zcmp_by_default_v6.1' into 'release/v6.1'

feat(build): add ZCMP workaround post-build check (v6.1)

See merge request espressif/esp-idf!51325
This commit is contained in:
Roland Dobai
2026-08-27 08:24:39 +02:00
20 changed files with 380 additions and 23 deletions

View File

@@ -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()

View File

@@ -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

View File

@@ -1,2 +1,5 @@
# Enable SPIRAM
CONFIG_SPIRAM=y
# TODO: IDF-14145
CONFIG_COMPILER_ENABLE_RISCV_ZCMP=n

View File

@@ -10,3 +10,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

View File

@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023-2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
@@ -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);

View File

@@ -30,6 +30,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;
@@ -168,6 +171,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);
}

View File

@@ -27,6 +27,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;
@@ -160,6 +163,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);
}

View File

@@ -25,6 +25,9 @@
#endif
#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;
@@ -178,6 +181,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);
}

View File

@@ -38,6 +38,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;
@@ -252,6 +255,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);
}

View File

@@ -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=$<TARGET_FILE:${target}>
-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=$<TARGET_FILE:$<GENEX_EVAL:${elf_target}>>
-DCMAKE_OBJDUMP=${CMAKE_OBJDUMP}
-P "${CMAKE_CURRENT_LIST_DIR}/check_zcmp_workaround.cmake"
COMMAND ${CMAKE_COMMAND} -E touch "${_riscv_zcmp_check_marker}"
DEPENDS "$<TARGET_FILE:$<GENEX_EVAL:${elf_target}>>"
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()

View File

@@ -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 <elf>.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()

View File

@@ -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'."

View File

@@ -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

View File

@@ -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)

View File

@@ -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``.

View File

@@ -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()

View File

@@ -0,0 +1,24 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdint.h>
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;
}

View File

@@ -0,0 +1 @@
CONFIG_COMPILER_ENABLE_RISCV_ZCMP=y

View File

@@ -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'

View File

@@ -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'