ci(esp_tee): Add fuzzing tests for verifying service call behaviour

This commit is contained in:
Laukik Hase
2026-03-11 18:03:58 +05:30
parent f1e8fe393a
commit 8abc7269d1
4 changed files with 190 additions and 2 deletions

View File

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

View File

@@ -0,0 +1,104 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdlib.h>
#include <string.h>
#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", "<count>", "Argument count");
fuzz_args.srv_id = arg_int1("i", "id", "<srv_id>", "Service ID");
fuzz_args.arg_list = arg_str0(NULL, NULL, "<arg_list>", "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);
}

View File

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

View File

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