From: Kyle Fox <kylefoxaustin.github@gmail.com>
To: qemu-devel@nongnu.org
Cc: Kyle Fox <kylefoxaustin.github@gmail.com>,
Paolo Bonzini <pbonzini@redhat.com>,
qemu-arm@nongnu.org (open list:MCIMX95-19X19-EVK...)
Subject: [PATCH 08/16] hw/misc: add NXP EdgeLock Enclave (ELE) responder
Date: Wed, 19 Aug 2026 21:48:26 -0500 [thread overview]
Message-ID: <20260820024834.3286721-9-kylefoxaustin.github@gmail.com> (raw)
In-Reply-To: <20260820024834.3286721-1-kylefoxaustin.github@gmail.com>
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 <kylefoxaustin.github@gmail.com>
---
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
next prev parent reply other threads:[~2026-08-20 2:50 UTC|newest]
Thread overview: 15+ messages / expand[flat|nested] mbox.gz Atom feed top
[not found] <20260820024834.3286721-1-kylefoxaustin.github@gmail.com>
2026-08-20 2:48 ` [PATCH 02/16] hw/arm/boot: let a board preset initrd_start Kyle Fox
2026-08-20 2:48 ` [PATCH 03/16] target/arm: opt-in align-down for a misaligned PMSAv7 MPU RBAR Kyle Fox
2026-08-20 2:48 ` [PATCH 04/16] hw/arm/armv7m: forward pmsav7-rbar-align-down to the CPU Kyle Fox
2026-08-20 2:48 ` [PATCH 05/16] hw/char: add i.MX LPUART Kyle Fox
2026-08-20 2:48 ` [PATCH 06/16] hw/i2c: add i.MX LPI2C Kyle Fox
2026-08-20 2:48 ` [PATCH 07/16] hw/misc: add i.MX Messaging Unit (MU v2) Kyle Fox
2026-08-20 2:48 ` Kyle Fox [this message]
2026-08-20 2:48 ` [PATCH 09/16] hw/timer: add i.MX 95 system counter Kyle Fox
2026-08-20 2:48 ` [PATCH 10/16] hw/misc: add i.MX 95 watchdog Kyle Fox
2026-08-20 2:48 ` [PATCH 11/16] hw/misc: add i.MX 95 ANATOP/AONMIX/GPC/SRC power and clock blocks Kyle Fox
2026-08-20 2:48 ` [PATCH 12/16] hw/misc: add i.MX 95 PMIC (PF09/PF53/PCAL6408A) and xcache controllers Kyle Fox
2026-08-20 2:48 ` [PATCH 13/16] hw/misc: add i.MX 95 DPU command-sequencer stub (headless) Kyle Fox
2026-08-20 2:48 ` [PATCH 14/16] hw/arm: add i.MX 95 SoC container (fsl-imx95) Kyle Fox
2026-08-20 2:48 ` [PATCH 15/16] hw/arm: add i.MX 95 19x19 EVK board Kyle Fox
2026-08-20 2:48 ` [PATCH 16/16] docs, MAINTAINERS, tests/functional: add i.MX 95 EVK Kyle Fox
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260820024834.3286721-9-kylefoxaustin.github@gmail.com \
--to=kylefoxaustin.github@gmail.com \
--cc=pbonzini@redhat.com \
--cc=qemu-arm@nongnu.org \
--cc=qemu-devel@nongnu.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox