diff --git a/components/esp_tee/test_apps/tee_test_fw/main/CMakeLists.txt b/components/esp_tee/test_apps/tee_test_fw/main/CMakeLists.txt index af0c5563825..dd3ed89ffc0 100644 --- a/components/esp_tee/test_apps/tee_test_fw/main/CMakeLists.txt +++ b/components/esp_tee/test_apps/tee_test_fw/main/CMakeLists.txt @@ -2,7 +2,7 @@ idf_build_get_property(idf_path IDF_PATH) set(priv_requires bootloader_support esp_driver_gptimer esp_tee esp_timer mbedtls spi_flash) # Test FW related -list(APPEND priv_requires nvs_flash test_utils unity) +list(APPEND priv_requires console nvs_flash test_utils unity) # TEE related list(APPEND priv_requires tee_sec_storage tee_ota_ops test_sec_srv) @@ -13,7 +13,8 @@ list(APPEND srcs "test_esp_tee_ctx_switch.c" "test_esp_tee_panic.c" "test_esp_tee_sec_stg.c" "test_esp_tee_ota.c" - "test_esp_tee_flash_prot.c") + "test_esp_tee_flash_prot.c" + "test_esp_tee_fuzzer.c") if(CONFIG_SECURE_TEE_ATTESTATION) list(APPEND srcs "test_esp_tee_att.c") diff --git a/components/esp_tee/test_apps/tee_test_fw/main/test_esp_tee_fuzzer.c b/components/esp_tee/test_apps/tee_test_fw/main/test_esp_tee_fuzzer.c new file mode 100644 index 00000000000..e44db84242a --- /dev/null +++ b/components/esp_tee/test_apps/tee_test_fw/main/test_esp_tee_fuzzer.c @@ -0,0 +1,104 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#include "esp_log.h" +#include "esp_err.h" +#include "esp_tee.h" +#include "esp_console.h" +#include "argtable3/argtable3.h" +#include "unity.h" + +#define MAX_ARGS 9 +#define MAX_CMD_FUZZ_LEN 384 + +static const char *TAG = "test_esp_tee_fuzzer"; + +static struct { + struct arg_int *arg_count; + struct arg_int *srv_id; + struct arg_str *arg_list; + struct arg_end *end; +} fuzz_args; + +static int fuzz(int argc, char **argv) +{ + int nerrors = arg_parse(argc, argv, (void **) &fuzz_args); + if (nerrors != 0) { + arg_print_errors(stderr, fuzz_args.end, argv[0]); + return ESP_ERR_INVALID_ARG; + } + + const int arg_count = fuzz_args.arg_count->ival[0]; + const int srv_id = fuzz_args.srv_id->ival[0]; + + if (arg_count < 0 || arg_count > MAX_ARGS) { + ESP_LOGE(TAG, "arg_count %d out of range [0, %d]", arg_count, MAX_ARGS); + return ESP_ERR_INVALID_ARG; + } + + uint32_t arg_val[MAX_ARGS] = {0}; + + if (arg_count > 0 && fuzz_args.arg_list->count > 0) { + char arg_list_buf[MAX_CMD_FUZZ_LEN + 1]; + strlcpy(arg_list_buf, fuzz_args.arg_list->sval[0], sizeof(arg_list_buf)); + + char *saveptr = NULL; + char *token = strtok_r(arg_list_buf, ";", &saveptr); + for (int i = 0; i < arg_count && token != NULL; i++) { + arg_val[i] = (uint32_t)strtoul(token, NULL, 0); + token = strtok_r(NULL, ";", &saveptr); + } + } + + /* + * All service arguments are passed as uint32_t through the variadic interface. + * The TEE dispatcher reads only (arg_count + 1) values; extra trailing args + * are harmless since they remain untouched in registers / on the stack. + * Skipping per-service type casts is intentional: for fuzzing we want the + * full 32-bit garbage to reach the service, not a truncated subset. + */ + uint32_t ret = esp_tee_service_call(arg_count + 1, srv_id, + arg_val[0], arg_val[1], arg_val[2], + arg_val[3], arg_val[4], arg_val[5], + arg_val[6]); + ESP_LOGI(TAG, "Service call returned 0x%08lx", ret); + + return ESP_OK; +} + +TEST_CASE("Test TEE service call Fuzzer", "[fuzzing]") +{ + esp_console_repl_t *repl = NULL; + esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT(); + repl_config.max_cmdline_length = MAX_CMD_FUZZ_LEN; + + esp_console_dev_uart_config_t uart_config = ESP_CONSOLE_DEV_UART_CONFIG_DEFAULT(); + TEST_ESP_OK(esp_console_new_repl_uart(&uart_config, &repl_config, &repl)); + + fuzz_args.arg_count = arg_int1("c", "count", "", "Argument count"); + fuzz_args.srv_id = arg_int1("i", "id", "", "Service ID"); + fuzz_args.arg_list = arg_str0(NULL, NULL, "", "Argument list"); + fuzz_args.end = arg_end(2); + + const esp_console_cmd_t fuzz_cmd = { + .command = "fuzz", + .help = "Fuzz TEE service", + .hint = NULL, + .func = &fuzz, + .argtable = &fuzz_args + }; + + TEST_ESP_OK(esp_console_cmd_register(&fuzz_cmd)); + TEST_ESP_OK(esp_console_register_help_command()); + TEST_ESP_OK(esp_console_start_repl(repl)); + + vTaskSuspend(NULL); +} diff --git a/components/esp_tee/test_apps/tee_test_fw/pytest_esp_tee_ut.py b/components/esp_tee/test_apps/tee_test_fw/pytest_esp_tee_ut.py index 1df6e3ecf8b..71910ca41a0 100644 --- a/components/esp_tee/test_apps/tee_test_fw/pytest_esp_tee_ut.py +++ b/components/esp_tee/test_apps/tee_test_fw/pytest_esp_tee_ut.py @@ -1,9 +1,14 @@ # SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 +import logging +import random import re from enum import Enum +from pathlib import Path +import pexpect import pytest +import yaml from pytest_embedded_idf import IdfDut from pytest_embedded_idf.utils import idf_parametrize from tee_exception_test_map import TEE_EXCEPTION_TEST_MAP @@ -506,3 +511,62 @@ def test_esp_tee_attestation(dut: IdfDut) -> None: # start test dut.run_all_single_board_cases(group='attestation') + + +# ---------------- TEE Fuzzing tests ---------------- + + +@pytest.mark.generic +@idf_parametrize('config', ['tee_fuzzing'], indirect=['config']) +@idf_parametrize('target', TESTING_TARGETS, indirect=['target']) +def test_esp_tee_fuzzer(dut: IdfDut) -> None: + # Panics are expected during fuzzing; skip GDB decode on teardown + dut.skip_decode_panic = True + + seed = random.randrange(2**32) + logging.info('Fuzzer seed: 0x%08x', seed) + rng = random.Random(seed) + + # Number of random inputs to send per service + FUZZ_ITERATIONS_PER_SERVICE = 4 + + # Build {service_id: arg_count} map from the target's YAML service table + yml_path = Path(__file__).parent.joinpath('..', '..', 'scripts', dut.target, 'sec_srv_tbl_default.yml').resolve() + with open(yml_path) as f: + services = { + entry['id']: entry['args'] for family in yaml.safe_load(f)['secure_services'] for entry in family['entries'] + } + + pat_ret = re.compile(r'Service call returned (0x[0-9A-Fa-f]+)') + pat_rst = re.compile(r'rst:(0x[0-9A-Fa-f]+) \(([^)]+)\)') + device_rebooted = True # True on first entry since device boots fresh + + for srv_id, num_args in sorted(services.items()): + for iter_idx in range(FUZZ_ITERATIONS_PER_SERVICE): + # Re-enter the console menu only after a reboot + if device_rebooted: + dut.expect_exact('Press ENTER to see the list of tests') + dut.write('[fuzzing]') + dut.expect('esp>') + device_rebooted = False + + # Always pass at least 1 arg; extra trailing args are ignored by the dispatcher + n = max(num_args, 1) + arg_str = ';'.join(f'0x{rng.getrandbits(32):08x}' for _ in range(n)) + dut.write(f'fuzz -c {num_args} -i {srv_id} {arg_str}') + + try: + # Service call either returns gracefully or crashes the device + match = dut.expect([pat_ret, pat_rst], timeout=3) + except pexpect.TIMEOUT: + logging.warning('Device hung, hard-resetting (srv_id: %d, iter: %d)', srv_id, iter_idx) + dut.serial.hard_reset() + device_rebooted = True + continue + + if pat_rst.match(match.group(0).decode()): + # Device crashed and rebooted — verify it was a software reset + device_rebooted = True + rst_rsn = match.group(2).decode() + if rst_rsn not in ('LP_SW_HPSYS', 'RTC_SW_HPSYS'): + logging.warning('Unexpected reset reason: "%s" (srv_id: %d, iter: %d)', rst_rsn, srv_id, iter_idx) diff --git a/components/esp_tee/test_apps/tee_test_fw/sdkconfig.ci.tee_fuzzing b/components/esp_tee/test_apps/tee_test_fw/sdkconfig.ci.tee_fuzzing new file mode 100644 index 00000000000..8804a96901a --- /dev/null +++ b/components/esp_tee/test_apps/tee_test_fw/sdkconfig.ci.tee_fuzzing @@ -0,0 +1,19 @@ +# Custom partition table +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_tee_ota.csv" +CONFIG_PARTITION_TABLE_FILENAME="partitions_tee_ota.csv" + +# Decreasing Bootloader and TEE log verbosity +CONFIG_BOOTLOADER_LOG_LEVEL_WARN=y +CONFIG_SECURE_TEE_LOG_LEVEL_WARN=y + +# Takes effect only for supported targets +CONFIG_SECURE_TEE_SEC_STG_SUPPORT_SECP384R1_SIGN=y + +# Enabling flash protection over SPI1 +CONFIG_SECURE_TEE_EXT_FLASH_MEMPROT_SPI1=y + +# Increasing TEE DRAM size +# 20KB +CONFIG_SECURE_TEE_DRAM_SIZE=0x5000