diff --git a/components/esp_common/include/esp_fault_internal.h b/components/esp_common/include/esp_fault_internal.h index 871f2e8e773..0a67e08d885 100644 --- a/components/esp_common/include/esp_fault_internal.h +++ b/components/esp_common/include/esp_fault_internal.h @@ -3,11 +3,12 @@ * * SPDX-License-Identifier: Apache-2.0 */ +#pragma once + +#include #include "sdkconfig.h" #include "esp_rom_sys.h" -#pragma once - #ifdef __cplusplus extern "C" { #endif @@ -19,6 +20,10 @@ extern "C" { * - Expands CONDITION multiple times (condition must have no side effects) * - Compiler is told all registers are invalid before evaluating CONDITION each time, to avoid a fault * causing a misread of a register used in all three evaluations of CONDITION. + * - The result of each evaluation is stored into a volatile variable and re-read before the branch. + * This prevents the compiler from constant-folding CONDITION and deleting the whole check when it + * has already proven the value - e.g. when this macro follows a normal "if (!cond) { ... }" check + * of the same value, which would otherwise silently remove the fault-injection protection. * - If CONDITION is ever false, a system reset is triggered. * * @note Place this macro after a "normal" check of CONDITION that will fail with a normal error @@ -40,13 +45,20 @@ extern "C" { * @param CONDITION A condition which will evaluate true unless an attacker used fault injection to skip or corrupt some other critical system calculation. * */ -#define ESP_FAULT_ASSERT(CONDITION) do { \ - asm volatile ("" ::: "memory"); \ - if(!(CONDITION)) _ESP_FAULT_RESET(); \ - asm volatile ("" ::: "memory"); \ - if(!(CONDITION)) _ESP_FAULT_RESET(); \ - asm volatile ("" ::: "memory"); \ - if(!(CONDITION)) _ESP_FAULT_RESET(); \ +#define ESP_FAULT_ASSERT(CONDITION) do { \ + bool esp_fault_assert_chk; \ + asm volatile ("" ::: "memory"); \ + esp_fault_assert_chk = (CONDITION); \ + asm volatile ("" : "+r"(esp_fault_assert_chk)); \ + if(!esp_fault_assert_chk) _ESP_FAULT_RESET(); \ + asm volatile ("" ::: "memory"); \ + esp_fault_assert_chk = (CONDITION); \ + asm volatile ("" : "+r"(esp_fault_assert_chk)); \ + if(!esp_fault_assert_chk) _ESP_FAULT_RESET(); \ + asm volatile ("" ::: "memory"); \ + esp_fault_assert_chk = (CONDITION); \ + asm volatile ("" : "+r"(esp_fault_assert_chk)); \ + if(!esp_fault_assert_chk) _ESP_FAULT_RESET(); \ } while(0) #if CONFIG_IDF_TARGET_ARCH_XTENSA diff --git a/components/esp_security/test_apps/.build-test-rules.yml b/components/esp_security/test_apps/.build-test-rules.yml index 6802faf7539..801962d89dc 100644 --- a/components/esp_security/test_apps/.build-test-rules.yml +++ b/components/esp_security/test_apps/.build-test-rules.yml @@ -5,3 +5,10 @@ components/esp_security/test_apps/crypto_drivers: - if: ((SOC_HMAC_SUPPORTED == 1) or (SOC_DIG_SIGN_SUPPORTED == 1)) or (SOC_KEY_MANAGER_SUPPORTED == 1) depends_components: - esp_security + - esp_hal_security + +components/esp_security/test_apps/fault_assert_opt_check: + enable: + - if: IDF_TARGET in ["esp32", "esp32c3"] # one Xtensa + one RISC-V + depends_components: + - esp_common diff --git a/components/esp_security/test_apps/fault_assert_opt_check/CMakeLists.txt b/components/esp_security/test_apps/fault_assert_opt_check/CMakeLists.txt new file mode 100644 index 00000000000..853b87d4eaf --- /dev/null +++ b/components/esp_security/test_apps/fault_assert_opt_check/CMakeLists.txt @@ -0,0 +1,19 @@ +# The following lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +# "Trim" the build. Include the minimal set of components, main, and anything it depends on. +set(COMPONENTS main) + +project(fault_assert_opt_check) + +# Regression guard: fail the build if ESP_FAULT_ASSERT() gets optimized away. +idf_build_get_property(python PYTHON) +add_custom_command( + TARGET ${CMAKE_PROJECT_NAME}.elf POST_BUILD + COMMAND ${python} "${CMAKE_CURRENT_SOURCE_DIR}/check_fault_asserts.py" + "$" "${CMAKE_OBJDUMP}" + COMMENT "Verifying ESP_FAULT_ASSERT() survived optimization" + VERBATIM) diff --git a/components/esp_security/test_apps/fault_assert_opt_check/README.md b/components/esp_security/test_apps/fault_assert_opt_check/README.md new file mode 100644 index 00000000000..1fb88efd154 --- /dev/null +++ b/components/esp_security/test_apps/fault_assert_opt_check/README.md @@ -0,0 +1,2 @@ +| Supported Targets | ESP32 | ESP32-C3 | +| ----------------- | ----- | -------- | diff --git a/components/esp_security/test_apps/fault_assert_opt_check/check_fault_asserts.py b/components/esp_security/test_apps/fault_assert_opt_check/check_fault_asserts.py new file mode 100644 index 00000000000..5d5994c33cf --- /dev/null +++ b/components/esp_security/test_apps/fault_assert_opt_check/check_fault_asserts.py @@ -0,0 +1,75 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 +"""Post-build regression guard for ESP_FAULT_ASSERT(). + +Disassembles the test app and verifies that the ESP_FAULT_ASSERT() calls in the +known test functions still emit their reset blocks. One intact assert produces +three independent reset-on-failure paths, i.e. three references to +``esp_rom_software_reset_system`` in the function. If the macro ever regresses +and the optimizer folds the checks away, the count drops and this exits non-zero, +failing the build. + +Usage: check_fault_asserts.py +""" +import re +import subprocess +import sys + +RESET_SYM = 'esp_rom_software_reset_system' + +# function name -> number of ESP_FAULT_ASSERT calls it contains (x3 reset blocks each) +EXPECTED = { + 'test_fa_guarded_flag': 1, + 'test_fa_guarded_status': 1, +} + +FUNC_RE = re.compile(r'^[0-9a-fA-F]+ <(.+)>:$') + + +def reset_counts(elf: str, objdump: str) -> dict: + dis = subprocess.run([objdump, '-d', elf], capture_output=True, text=True, check=True).stdout + counts: dict = {} + cur = None + for line in dis.splitlines(): + m = FUNC_RE.match(line) + if m: + cur = m.group(1) + counts.setdefault(cur, 0) + elif cur and RESET_SYM in line: + counts[cur] += 1 + return counts + + +def main() -> int: + if len(sys.argv) != 3: + print(__doc__) + return 2 + elf, objdump = sys.argv[1], sys.argv[2] + counts = reset_counts(elf, objdump) + + failed = False + for fn, n_asserts in EXPECTED.items(): + expected = 3 * n_asserts + found = counts.get(fn) + if found is None: + print(f'ERROR: {fn} not found in {elf} (renamed/removed?)') + failed = True + elif found < expected: + print( + f'ERROR: ESP_FAULT_ASSERT optimized away in {fn}: ' + f'{found} reset checks, expected {expected}. ' + f'See components/esp_common/include/esp_fault.h' + ) + failed = True + else: + print(f'OK: {fn} -> {found} reset checks ({found // 3} assert(s) x3)') + + if failed: + print('FAILED: ESP_FAULT_ASSERT regression check') + return 1 + print('PASSED: ESP_FAULT_ASSERT checks survived optimization') + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/components/esp_security/test_apps/fault_assert_opt_check/main/CMakeLists.txt b/components/esp_security/test_apps/fault_assert_opt_check/main/CMakeLists.txt new file mode 100644 index 00000000000..d71200c0946 --- /dev/null +++ b/components/esp_security/test_apps/fault_assert_opt_check/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "test_fault_assert.c" + INCLUDE_DIRS ".") diff --git a/components/esp_security/test_apps/fault_assert_opt_check/main/test_fault_assert.c b/components/esp_security/test_apps/fault_assert_opt_check/main/test_fault_assert.c new file mode 100644 index 00000000000..920c09ecb01 --- /dev/null +++ b/components/esp_security/test_apps/fault_assert_opt_check/main/test_fault_assert.c @@ -0,0 +1,58 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/* + * Regression guard for ESP_FAULT_ASSERT() being silently optimised away. + * + * ESP_FAULT_ASSERT(C) must emit three independent "evaluate C -> reset if false" + * checks. When C is a value the optimiser can already prove (e.g. a flag pinned + * by a preceding "if (!C) return"), a naive implementation lets GCC constant-fold + * C and delete all three checks, removing the fault-injection protection with no + * warning. + * + * The functions below place ESP_FAULT_ASSERT in exactly that + * "proven-true cached value" shape. check_fault_asserts.py disassembles the built + * app and fails the build if any of them lost its reset blocks. Keep them + * noinline+used so each is an independent symbol the checker can find. + */ + +#include +#include "esp_fault.h" +#include "esp_attr.h" + +/* volatile so the initial value is opaque: only the early-return "proves" it, + * which is the precise condition that triggers the optimiser elimination. */ +volatile bool fa_test_flag = true; +volatile int fa_test_status = 0; +volatile int fa_test_sink; + +/* Cached bool guarded by an early return -> the original elimination case. */ +bool NOINLINE_ATTR test_fa_guarded_flag(void) +{ + bool valid = fa_test_flag; + if (!valid) { + return false; + } + ESP_FAULT_ASSERT(valid); + return true; +} + +/* Cached status compared to a constant, guarded by an early return. */ +int NOINLINE_ATTR test_fa_guarded_status(void) +{ + int status = fa_test_status; + if (status != 0) { + return status; + } + ESP_FAULT_ASSERT(status == 0); + return 0; +} + +void app_main(void) +{ + /* Reference the test functions so they are linked (not GC'd). */ + fa_test_sink = (int)test_fa_guarded_flag() + test_fa_guarded_status(); +} diff --git a/components/esp_security/test_apps/fault_assert_opt_check/sdkconfig.ci.opt_perf b/components/esp_security/test_apps/fault_assert_opt_check/sdkconfig.ci.opt_perf new file mode 100644 index 00000000000..4de2cc6f3ad --- /dev/null +++ b/components/esp_security/test_apps/fault_assert_opt_check/sdkconfig.ci.opt_perf @@ -0,0 +1,2 @@ +# -O2 +CONFIG_COMPILER_OPTIMIZATION_PERF=y diff --git a/components/esp_security/test_apps/fault_assert_opt_check/sdkconfig.ci.opt_size b/components/esp_security/test_apps/fault_assert_opt_check/sdkconfig.ci.opt_size new file mode 100644 index 00000000000..f6da2fb1fbc --- /dev/null +++ b/components/esp_security/test_apps/fault_assert_opt_check/sdkconfig.ci.opt_size @@ -0,0 +1,2 @@ +# -Os +CONFIG_COMPILER_OPTIMIZATION_SIZE=y diff --git a/components/esp_security/test_apps/fault_assert_opt_check/sdkconfig.defaults b/components/esp_security/test_apps/fault_assert_opt_check/sdkconfig.defaults new file mode 100644 index 00000000000..72548452f94 --- /dev/null +++ b/components/esp_security/test_apps/fault_assert_opt_check/sdkconfig.defaults @@ -0,0 +1,4 @@ +# The clang-built esp32 (Xtensa) bootloader is slightly larger than the GCC one and +# overflows the default 0x7000 limit; move the partition table offset to give it room. +# Harmless for GCC builds. +CONFIG_PARTITION_TABLE_OFFSET=0x9000 diff --git a/components/spi_flash/test_apps/flash_encryption/partitions.csv b/components/spi_flash/test_apps/flash_encryption/partitions.csv index c941d8f4f1c..f5933e6f71f 100644 --- a/components/spi_flash/test_apps/flash_encryption/partitions.csv +++ b/components/spi_flash/test_apps/flash_encryption/partitions.csv @@ -1,5 +1,5 @@ # Name, Type, SubType, Offset, Size, Flags # Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap -nvs, data, nvs, 0x9000, 0x6000, -factory, 0, 0, 0x10000, 1M +nvs, data, nvs, , 0x6000, +factory, 0, 0, , 1M flash_test, data, fat, , 528K diff --git a/components/spi_flash/test_apps/flash_encryption/sdkconfig.defaults b/components/spi_flash/test_apps/flash_encryption/sdkconfig.defaults index d0ef86b66cf..c41595c93ec 100644 --- a/components/spi_flash/test_apps/flash_encryption/sdkconfig.defaults +++ b/components/spi_flash/test_apps/flash_encryption/sdkconfig.defaults @@ -1,4 +1,5 @@ CONFIG_ESP_TASK_WDT_EN=n +CONFIG_PARTITION_TABLE_OFFSET=0X9000 CONFIG_PARTITION_TABLE_CUSTOM=y CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" CONFIG_SECURE_FLASH_ENC_ENABLED=y diff --git a/examples/system/efuse/sdkconfig.ci.virt_secure_boot_v2.esp32c5 b/examples/system/efuse/sdkconfig.ci.virt_secure_boot_v2.esp32c5 index 08ebca9f6ba..2f243f622ae 100644 --- a/examples/system/efuse/sdkconfig.ci.virt_secure_boot_v2.esp32c5 +++ b/examples/system/efuse/sdkconfig.ci.virt_secure_boot_v2.esp32c5 @@ -2,7 +2,7 @@ CONFIG_IDF_TARGET="esp32c5" -CONFIG_PARTITION_TABLE_OFFSET=0xD000 +CONFIG_PARTITION_TABLE_OFFSET=0xE000 CONFIG_PARTITION_TABLE_CUSTOM=y CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="test/partitions_efuse_emul.csv" diff --git a/examples/system/efuse/sdkconfig.ci.virt_secure_boot_v2.esp32p4 b/examples/system/efuse/sdkconfig.ci.virt_secure_boot_v2.esp32p4 index 5305d602c62..1b83c3d5946 100644 --- a/examples/system/efuse/sdkconfig.ci.virt_secure_boot_v2.esp32p4 +++ b/examples/system/efuse/sdkconfig.ci.virt_secure_boot_v2.esp32p4 @@ -2,7 +2,7 @@ CONFIG_IDF_TARGET="esp32p4" -CONFIG_PARTITION_TABLE_OFFSET=0xD000 +CONFIG_PARTITION_TABLE_OFFSET=0XE000 CONFIG_PARTITION_TABLE_CUSTOM=y CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="test/partitions_efuse_emul.csv"