Files
esp-idf/components/riscv/check_zcmp_workaround.cmake
Alexey Lapshin 631ad9033c 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.
2026-07-22 13:04:20 +07:00

180 lines
6.6 KiB
CMake

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