[PATCH 08/16] hw/misc: add NXP EdgeLock Enclave (ELE) responder
Kyle Fox <[email protected]>
| Newsgroups | org.nongnu.qemu-arm,org.nongnu.qemu-devel |
|---|---|
| Message-ID | <[email protected]> |
The NXP EdgeLock Enclave (ELE) responder. U-Boot and the SM firmware issue ELE service requests (e.g. GET_INFO, get-random) over a dedicated MU; this models the responder that consumes the request words and returns a response so the secure bring-up sequence proceeds. It is linked to its MU through a QOM link property. Signed-off-by: Kyle Fox <[email protected]> --- hw/misc/Kconfig | 4 + hw/misc/imx95_ele_server.c | 284 +++++++++++++++++++++++++++++ hw/misc/meson.build | 1 + hw/misc/trace-events | 2 + include/hw/misc/imx95_ele_server.h | 47 +++++ 5 files changed, 338 insertions(+) create mode 100644 hw/misc/imx95_ele_server.c create mode 100644 include/hw/misc/imx95_ele_server.h diff --git a/hw/misc/Kconfig b/hw/misc/Kconfig index 8645ca11f3f..ded60e21a6c 100644 --- a/hw/misc/Kconfig +++ b/hw/misc/Kconfig @@ -264,3 +264,7 @@ source macio/Kconfig config IMX_MU bool + +config IMX95_ELE_SERVER + bool + select IMX_MU diff --git a/hw/misc/imx95_ele_server.c b/hw/misc/imx95_ele_server.c new file mode 100644 index 00000000000..b8ee3f78b61 --- /dev/null +++ b/hw/misc/imx95_ele_server.c @@ -0,0 +1,284 @@ +/* + * Minimal NXP EdgeLock Enclave (ELE) responder stub + * + * Copyright (c) 2026, Kyle Fox + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + * Watches TR-register writes on an i.MX MU, accumulates ELE-protocol + * command words until the per-message size is reached, dispatches to a + * command handler, and writes the response into RR registers + asserts + * RSR.RFn so the agent's mu_hal_receivemsg() poll exits. + * + * ELE message format (per ele_api.h): + * + * word 0 (header): version[7:0] | size[15:8] | command[23:16] | tag[31:24] + * word 1..N-1: payload + * + * size = total words including header. tag = ELE_CMD_TAG (0x17) for + * commands, ELE_RESP_TAG (0xE1) for responses. Responses also have a + * status byte (ELE_SUCCESS_IND = 0xD6 for success) somewhere in the + * payload depending on command. + * + * Only ELE_GET_INFO_REQ is implemented in detail (the + * only ELE call U-Boot SPL's imx9_probe_mu() makes pre-relocation, + * via ele_get_info()). Other commands get a generic SUCCESS response. + */ + +#include "qemu/osdep.h" +#include "qemu/log.h" +#include "qemu/main-loop.h" +#include "qemu/module.h" +#include "qapi/error.h" +#include "hw/misc/imx95_ele_server.h" +#include "hw/core/qdev-properties.h" +#include "hw/core/qdev-properties-system.h" +#include "system/dma.h" +#include "migration/vmstate.h" +#include "trace.h" + +/* + * struct ele_get_info_data layout (u32 word offsets), pinned to the + * U-Boot ele_api.h definition at + * U-Boot arch/arm/include/asm/mach-imx/ele_api.h:168. + * Using named offsets here so a future U-Boot rev that reorders or + * adds fields trips a build/runtime mismatch instead of silently + * writing into the wrong slot. + * + * Reference layout: + * u32 hdr; // word 0 + * u32 soc; // word 1 + * u32 lc; // word 2 + * u32 uid[4]; // words 3..6 + * u32 sha256_rom_patch[8]; // words 7..14 + * u32 sha_fw[8]; // words 15..22 + * u32 oem_srkh[16]; // words 23..38 + * u32 state; // word 39 + * u32 oem_pqc_srkh[16]; // words 40..55 + * u32 reserved[8]; // words 56..63 + * Total: 64 u32 = 256 bytes. + */ +#define ELE_INFO_OFFSET_HDR 0 +#define ELE_INFO_OFFSET_SOC 1 +#define ELE_INFO_OFFSET_LC 2 +#define ELE_INFO_OFFSET_UID 3 +#define ELE_INFO_OFFSET_SHA256_ROM 7 +#define ELE_INFO_OFFSET_SHA_FW 15 +#define ELE_INFO_OFFSET_OEM_SRKH 23 +#define ELE_INFO_OFFSET_STATE 39 +#define ELE_INFO_OFFSET_OEM_PQC_SRKH 40 +#define ELE_INFO_OFFSET_RESERVED 56 +#define ELE_INFO_SIZE_WORDS 64 +#define ELE_INFO_SIZE_BYTES (ELE_INFO_SIZE_WORDS * 4) + +/* Header field packing. */ +static inline uint32_t ele_make_header(uint8_t version, uint8_t size, + uint8_t command, uint8_t tag) +{ + return (uint32_t)version | + ((uint32_t)size << 8) | + ((uint32_t)command << 16) | + ((uint32_t)tag << 24); +} + +/* Header field extraction. */ +#define ELE_HDR_VERSION(h) ((uint8_t)((h) & 0xFF)) +#define ELE_HDR_SIZE(h) ((uint8_t)(((h) >> 8) & 0xFF)) +#define ELE_HDR_COMMAND(h) ((uint8_t)(((h) >> 16) & 0xFF)) +#define ELE_HDR_TAG(h) ((uint8_t)(((h) >> 24) & 0xFF)) + +/* + * ELE_GET_INFO_REQ handler. + * + * Request layout (4 words): + * [0] header (command = ELE_GET_INFO_REQ) + * [1] info_addr_hi (upper 32 bits of guest pointer) + * [2] info_addr_lo (lower 32 bits of guest pointer) + * [3] size of ele_get_info_data + * + * Real ELE writes a struct ele_get_info_data to the guest address. + * We fake plausible values: rev 0xA1 i.MX 95, OEM-open lifecycle, + * UID zero-filled. The Linux/U-Boot drivers do not validate beyond + * memcpy into gd->arch. + * + * Response: 2 words. Status word ELE_SUCCESS_IND in data[0]. + */ +static void ele_handle_get_info(IMX95ELEServerState *s) +{ + if (s->msg_count < 4) { + qemu_log_mask(LOG_GUEST_ERROR, + "%s: GET_INFO with only %u words received\n", + __func__, s->msg_count); + return; + } + + uint64_t info_addr = ((uint64_t)s->msg_buf[1] << 32) | + (uint64_t)s->msg_buf[2]; + + /* + * Zero-fill the full 256-byte struct, then set the fields U-Boot's + * set_cpu_info() reads (soc, lc). Named offsets above keep the + * mapping legible; if U-Boot ever shifts the struct layout, the + * build check below + a re-read of ele_api.h are how this gets + * detected. + */ + uint32_t info_data[ELE_INFO_SIZE_WORDS] = {0}; + QEMU_BUILD_BUG_ON(sizeof(info_data) != ELE_INFO_SIZE_BYTES); + + info_data[ELE_INFO_OFFSET_SOC] = 0xA1009500; /* SoC rev 0xA1, type 0x95 */ + info_data[ELE_INFO_OFFSET_LC] = 0x00000080; /* lifecycle = OEM open */ + + dma_memory_write(&address_space_memory, info_addr, + info_data, sizeof(info_data), + MEMTXATTRS_UNSPECIFIED); + + /* Response: header + status. */ + uint32_t resp_hdr = ele_make_header(ELE_VERSION, 2, + ELE_GET_INFO_REQ, ELE_RESP_TAG); + trace_imx95_ele_response(ELE_GET_INFO_REQ); + imx_mu_deliver_rr(s->mu, 0, resp_hdr); + imx_mu_deliver_rr(s->mu, 1, ELE_SUCCESS_IND); +} + +/* + * Generic SUCCESS response for any command we do not specifically + * handle. 2-word response: header + ELE_SUCCESS_IND. + */ +static void ele_handle_generic_ok(IMX95ELEServerState *s, uint8_t command) +{ + uint32_t resp_hdr = ele_make_header(ELE_VERSION, 2, + command, ELE_RESP_TAG); + trace_imx95_ele_response(command); + imx_mu_deliver_rr(s->mu, 0, resp_hdr); + imx_mu_deliver_rr(s->mu, 1, ELE_SUCCESS_IND); +} + +static void ele_dispatch(IMX95ELEServerState *s) +{ + uint32_t header = s->msg_buf[0]; + uint8_t command = ELE_HDR_COMMAND(header); + uint8_t tag = ELE_HDR_TAG(header); + + trace_imx95_ele_msg(command, tag, s->msg_size); + + switch (command) { + case ELE_GET_INFO_REQ: + ele_handle_get_info(s); + return; + default: + qemu_log_mask(LOG_UNIMP, + "%s: cmd 0x%02x not handled in detail; responding OK\n", + __func__, command); + ele_handle_generic_ok(s, command); + return; + } +} + +/* + * TR-write callback. Invoked synchronously by the MU model from inside + * the guest's MMIO write to TR[idx]. The MU itself re-sets TSR.TEn + * after this returns so the next TR write succeeds without polling. + */ +static void ele_on_tr_write(void *opaque, unsigned int idx, uint32_t value) +{ + IMX95ELEServerState *s = opaque; + + /* Invoked from the MU MMIO write handler, which runs under the BQL. */ + assert(bql_locked()); + + if (s->msg_count >= IMX95_ELE_MAX_WORDS) { + qemu_log_mask(LOG_GUEST_ERROR, + "%s: too many words; resetting accumulator\n", + __func__); + s->msg_count = 0; + s->msg_size = 0; + return; + } + + s->msg_buf[s->msg_count++] = value; + + if (s->msg_count == 1) { + /* First word is the header; pick out size. */ + s->msg_size = ELE_HDR_SIZE(value); + if (s->msg_size == 0 || s->msg_size > IMX95_ELE_MAX_WORDS) { + qemu_log_mask(LOG_GUEST_ERROR, + "%s: bad size %u in header 0x%08x\n", + __func__, s->msg_size, value); + s->msg_count = 0; + s->msg_size = 0; + return; + } + } + + if (s->msg_count == s->msg_size) { + ele_dispatch(s); + s->msg_count = 0; + s->msg_size = 0; + } +} + +static void imx95_ele_server_reset_hold(Object *obj, ResetType type) +{ + IMX95ELEServerState *s = IMX95_ELE_SERVER(obj); + + s->msg_count = 0; + s->msg_size = 0; + memset(s->msg_buf, 0, sizeof(s->msg_buf)); +} + +static void imx95_ele_server_realize(DeviceState *dev, Error **errp) +{ + IMX95ELEServerState *s = IMX95_ELE_SERVER(dev); + + if (!s->mu) { + error_setg(errp, "%s: 'mu' link property must be set", + TYPE_IMX95_ELE_SERVER); + return; + } + imx_mu_set_tr_write_handler(s->mu, ele_on_tr_write, s); +} + +static const Property imx95_ele_server_properties[] = { + DEFINE_PROP_LINK("mu", IMX95ELEServerState, mu, + TYPE_IMX_MU, IMXMUState *), +}; + +static const VMStateDescription vmstate_imx95_ele_server = { + .name = TYPE_IMX95_ELE_SERVER, + .version_id = 1, + .minimum_version_id = 1, + .fields = (const VMStateField[]) { + VMSTATE_UINT32(msg_count, IMX95ELEServerState), + VMSTATE_UINT32(msg_size, IMX95ELEServerState), + VMSTATE_UINT32_ARRAY(msg_buf, IMX95ELEServerState, + IMX95_ELE_MAX_WORDS), + VMSTATE_END_OF_LIST() + }, +}; + +static void imx95_ele_server_class_init(ObjectClass *klass, const void *data) +{ + DeviceClass *dc = DEVICE_CLASS(klass); + ResettableClass *rc = RESETTABLE_CLASS(klass); + + dc->realize = imx95_ele_server_realize; + dc->vmsd = &vmstate_imx95_ele_server; + rc->phases.hold = imx95_ele_server_reset_hold; + set_bit(DEVICE_CATEGORY_MISC, dc->categories); + dc->desc = "NXP i.MX 95 ELE responder (get_info)"; + device_class_set_props(dc, imx95_ele_server_properties); +} + +static const TypeInfo imx95_ele_server_info = { + .name = TYPE_IMX95_ELE_SERVER, + .parent = TYPE_SYS_BUS_DEVICE, + .instance_size = sizeof(IMX95ELEServerState), + .class_init = imx95_ele_server_class_init, +}; + +static void imx95_ele_server_register_types(void) +{ + type_register_static(&imx95_ele_server_info); +} + +type_init(imx95_ele_server_register_types) diff --git a/hw/misc/meson.build b/hw/misc/meson.build index 9d0e3ed220d..b0dade72c6c 100644 --- a/hw/misc/meson.build +++ b/hw/misc/meson.build @@ -172,3 +172,4 @@ system_ss.add(when: 'CONFIG_LASI', if_true: files('lasi.c')) system_ss.add(when: 'CONFIG_AXIADO_CLK', if_true: files('axiado_clk.c')) system_ss.add(when: 'CONFIG_IMX_MU', if_true: files('imx_mu.c')) +system_ss.add(when: 'CONFIG_IMX95_ELE_SERVER', if_true: files('imx95_ele_server.c')) diff --git a/hw/misc/trace-events b/hw/misc/trace-events index 01b0b275f7f..6b3cff74549 100644 --- a/hw/misc/trace-events +++ b/hw/misc/trace-events @@ -446,3 +446,5 @@ imx_mu_tr_write(unsigned idx, uint32_t val) "TR[%u] <- 0x%08x" imx_mu_rr_deliver(unsigned idx, uint32_t val) "RR[%u] <- 0x%08x" imx_mu_doorbell(unsigned idx) "doorbell GIR channel %u" imx_mu_gip(unsigned idx) "GIP assert channel %u" +imx95_ele_msg(uint8_t command, uint8_t tag, uint32_t size) "received cmd 0x%02x tag 0x%02x size %u" +imx95_ele_response(uint8_t command) "response cmd 0x%02x" diff --git a/include/hw/misc/imx95_ele_server.h b/include/hw/misc/imx95_ele_server.h new file mode 100644 index 00000000000..75e6dfa4e25 --- /dev/null +++ b/include/hw/misc/imx95_ele_server.h @@ -0,0 +1,47 @@ +/* + * Minimal NXP EdgeLock Enclave (ELE) responder stub + * + * Copyright (c) 2026, Kyle Fox + * + * SPDX-License-Identifier: GPL-2.0-or-later + * + * Watches an i.MX MU for ELE-protocol command words, dispatches them + * to handlers, and writes responses back through the MU's RR + * registers. Implements only ele_get_info() (the one ELE call + * U-Boot SPL's imx9_probe_mu() makes pre-relocation). Everything + * else returns ELE_OK with a stub response so SPL doesn't panic. + */ + +#ifndef IMX95_ELE_SERVER_H +#define IMX95_ELE_SERVER_H + +#include "hw/core/sysbus.h" +#include "qom/object.h" +#include "hw/misc/imx_mu.h" + +#define TYPE_IMX95_ELE_SERVER "imx95.ele-server" +OBJECT_DECLARE_SIMPLE_TYPE(IMX95ELEServerState, IMX95_ELE_SERVER) + +/* ELE protocol constants from arch/arm/include/asm/mach-imx/ele_api.h. */ +#define ELE_VERSION 0x06 +#define ELE_CMD_TAG 0x17 +#define ELE_RESP_TAG 0xE1 +#define ELE_GET_INFO_REQ 0xDA +#define ELE_SUCCESS_IND 0xD6 + +/* Maximum words in a single ELE message (per ELE_MAX_MSG in U-Boot). */ +#define IMX95_ELE_MAX_WORDS 32 + +struct IMX95ELEServerState { + SysBusDevice parent_obj; + + /* Link to the MU used as ELE transport (elemu1). */ + IMXMUState *mu; + + /* Accumulator for incoming command words. */ + uint32_t msg_buf[IMX95_ELE_MAX_WORDS]; + uint32_t msg_count; + uint32_t msg_size; +}; + +#endif /* IMX95_ELE_SERVER_H */ -- 2.34.1