[RFC PATCH v1 6/6] tee: optee: add a RISC-V conduit over the RPMI TEE service group

From: marouene . boubakri

Date: Wed Sep 09 2026 - 21:10:04 EST


From: Marouene Boubakri <marouene.boubakri@xxxxxxxxxxx>

On RISC-V, OP-TEE runs as a supervisor domain isolated from the Rich
Execution Environment by the M-mode firmware. There is no SMC or HVC
instruction to reach it: the kernel invokes OP-TEE through the TEE
service group of the RISC-V Platform Management Interface (RPMI) v2.0,
whose TEE_CALL service is carried by the SBI Message Proxy (MPXY)
extension of SBI v3.0. The M-mode firmware implementing the service
group (the RPMI TEE framework) switches the calling hart to the OP-TEE
domain until OP-TEE responds, so a TEE_CALL behaves exactly like an
SMC: it runs on the calling hart and returns when OP-TEE completes the
call, requests an RPC or yields on a foreign interrupt.

Add a conduit for the SMC ABI which sends the register arguments a0-a7
of each invocation as the service data of a TEE_CALL request and takes
the return values a0-a3 from its service response, using the OP-TEE API
UID as the service UUID. The TEE_CALL request and response follow the
specification, only the content of the service data and of the service
response, which the specification leaves to the service, is defined
here. The message layout is in optee_rpmi.h which is kept in sync with
OP-TEE OS like optee_smc.h and optee_msg.h. The SMC ABI itself, the
message protocol, the RPC handling, dynamic and static shared memory
and notifications are unchanged.

The conduit binds to a "linaro,optee-rpmi" firmware node referencing the
MPXY mailbox channel and carrying the REE and OP-TEE endpoint
identifiers assigned by the framework. The channel is requested through
the mailbox core to claim it, and its RPMI attributes are checked at
probe, but TEE_CALL requests are sent with riscv_sbi_mpxy_mbox_call()
rather than mbox_send_message(): OP-TEE executes on the calling hart for
an unbounded time, which cannot happen under the mailbox channel
spinlock with interrupts disabled. Errors reported by the SBI
implementation or the framework, which mean that OP-TEE was not
reached, are converted into OPTEE_SMC_RETURN_EBUSY or
OPTEE_SMC_RETURN_ENOTAVAIL so that the callers see a failed call.

Signed-off-by: Marouene Boubakri <marouene.boubakri@xxxxxxxxxxx>
---
Documentation/tee/op-tee.rst | 18 ++-
drivers/tee/Kconfig | 2 +-
drivers/tee/optee/Kconfig | 11 +-
drivers/tee/optee/Makefile | 1 +
drivers/tee/optee/optee_private.h | 3 +
drivers/tee/optee/optee_rpmi.h | 69 ++++++++++
drivers/tee/optee/rpmi_conduit.c | 203 ++++++++++++++++++++++++++++++
drivers/tee/optee/smc_abi.c | 9 ++
8 files changed, 312 insertions(+), 4 deletions(-)
create mode 100644 drivers/tee/optee/optee_rpmi.h
create mode 100644 drivers/tee/optee/rpmi_conduit.c

diff --git a/Documentation/tee/op-tee.rst b/Documentation/tee/op-tee.rst
index b0ac097d5..b56c32bf2 100644
--- a/Documentation/tee/op-tee.rst
+++ b/Documentation/tee/op-tee.rst
@@ -4,14 +4,24 @@
OP-TEE (Open Portable Trusted Execution Environment)
====================================================

-The OP-TEE driver handles OP-TEE [1] based TEEs. Currently it is only the ARM
-TrustZone based OP-TEE solution that is supported.
+The OP-TEE driver handles OP-TEE [1] based TEEs. The ARM TrustZone based
+OP-TEE solution and OP-TEE running as an isolated supervisor domain on
+RISC-V are supported.

Lowest level of communication with OP-TEE builds on ARM SMC Calling
Convention (SMCCC) [2], which is the foundation for OP-TEE's SMC interface
[3] used internally by the driver. Stacked on top of that is OP-TEE Message
Protocol [4].

+On RISC-V the SMC interface is unchanged but its register arguments and
+return values are carried by the TEE_CALL service of the TEE service group
+of the RISC-V Platform Management Interface (RPMI) [7], sent to the M-mode
+firmware on an SBI Message Proxy (MPXY) channel [8]. The M-mode firmware
+switches the calling hart to the OP-TEE domain until OP-TEE responds, so a
+call behaves like an SMC: it runs on the calling hart and returns when
+OP-TEE completes, requests an RPC or yields on a foreign interrupt. The
+message layout is described in drivers/tee/optee/optee_rpmi.h.
+
OP-TEE SMC interface provides the basic functions required by SMCCC and some
additional functions specific for OP-TEE. The most interesting functions are:

@@ -164,3 +174,7 @@ References
"TEE Client API Specification v1.0" and click download.

[6] https://trustedfirmware-a.readthedocs.io/en/latest/threat_model/threat_model.html
+
+[7] https://github.com/riscv-non-isa/riscv-rpmi/releases
+
+[8] https://github.com/riscv-non-isa/riscv-sbi-doc/releases
diff --git a/drivers/tee/Kconfig b/drivers/tee/Kconfig
index 98c3ad083..6434204d8 100644
--- a/drivers/tee/Kconfig
+++ b/drivers/tee/Kconfig
@@ -2,7 +2,7 @@
# Generic Trusted Execution Environment Configuration
menuconfig TEE
tristate "Trusted Execution Environment support"
- depends on HAVE_ARM_SMCCC || COMPILE_TEST || CPU_SUP_AMD
+ depends on HAVE_ARM_SMCCC || COMPILE_TEST || CPU_SUP_AMD || RISCV_SBI_MPXY_MBOX
select CRYPTO_LIB_SHA1
select DMA_SHARED_BUFFER
select GENERIC_ALLOCATOR
diff --git a/drivers/tee/optee/Kconfig b/drivers/tee/optee/Kconfig
index 50d2051f7..0b6cd91a8 100644
--- a/drivers/tee/optee/Kconfig
+++ b/drivers/tee/optee/Kconfig
@@ -2,13 +2,22 @@
# OP-TEE Trusted Execution Environment Configuration
config OPTEE
tristate "OP-TEE"
- depends on HAVE_ARM_SMCCC
+ depends on HAVE_ARM_SMCCC || RISCV_SBI_MPXY_MBOX
+ depends on RISCV_SBI_MPXY_MBOX || !RISCV_SBI_MPXY_MBOX
depends on MMU
depends on RPMB || !RPMB
help
This implements the OP-TEE Trusted Execution Environment (TEE)
driver.

+config OPTEE_RPMI_CONDUIT
+ bool
+ depends on OPTEE && RISCV_SBI_MPXY_MBOX
+ default y
+ help
+ Reach OP-TEE through the TEE service group of the RISC-V Platform
+ Management Interface (RPMI) on an SBI Message Proxy (MPXY) channel.
+
config OPTEE_INSECURE_LOAD_IMAGE
bool "Load OP-TEE image as firmware"
default n
diff --git a/drivers/tee/optee/Makefile b/drivers/tee/optee/Makefile
index ad7049c1c..4b5a868a9 100644
--- a/drivers/tee/optee/Makefile
+++ b/drivers/tee/optee/Makefile
@@ -9,6 +9,7 @@ optee-objs += supp.o
optee-objs += device.o
optee-objs += smc_abi.o
optee-objs += ffa_abi.o
+optee-$(CONFIG_OPTEE_RPMI_CONDUIT) += rpmi_conduit.o

# for tracing framework to find optee_trace.h
CFLAGS_smc_abi.o := -I$(src)
diff --git a/drivers/tee/optee/optee_private.h b/drivers/tee/optee/optee_private.h
index aefe1e6f5..b90359988 100644
--- a/drivers/tee/optee/optee_private.h
+++ b/drivers/tee/optee/optee_private.h
@@ -424,6 +424,9 @@ static inline void reg_pair_from_64(u32 *reg0, u32 *reg1, u64 val)
/* Registration of the ABIs */
int optee_smc_abi_register(void);
void optee_smc_abi_unregister(void);
+#if IS_ENABLED(CONFIG_OPTEE_RPMI_CONDUIT)
+optee_invoke_fn *optee_rpmi_conduit_init(struct device *dev);
+#endif
int optee_ffa_abi_register(void);
void optee_ffa_abi_unregister(void);

diff --git a/drivers/tee/optee/optee_rpmi.h b/drivers/tee/optee/optee_rpmi.h
new file mode 100644
index 000000000..89821255b
--- /dev/null
+++ b/drivers/tee/optee/optee_rpmi.h
@@ -0,0 +1,69 @@
+/* SPDX-License-Identifier: (GPL-2.0 OR BSD-2-Clause) */
+/*
+ * Copyright 2026 NXP
+ */
+#ifndef OPTEE_RPMI_H
+#define OPTEE_RPMI_H
+
+#include <linux/types.h>
+#include <linux/uuid.h>
+
+/*
+ * This file defines how the OP-TEE SMC ABI (optee_smc.h) is carried by the
+ * TEE service group of the RISC-V Platform Management Interface (RPMI). It
+ * is kept in sync between secure world and the normal world driver.
+ *
+ * An invocation of the SMC ABI is a TEE_CALL service request sent to the
+ * RPMI TEE framework on an SBI MPXY channel where:
+ * - SENDER_ID identifies the REE endpoint and TARGET_ID the OP-TEE
+ * endpoint, both assigned by the framework,
+ * - SERVICE is OPTEE_RPMI_SERVICE_UUID in RFC 4122 byte order,
+ * - SERVICE_DATA carries the register arguments a0-a7 of the SMC ABI as
+ * little-endian 64-bit words.
+ *
+ * The SERVICE_RSP of the TEE_CALL response carries the return values a0-a3
+ * of the SMC ABI as little-endian 64-bit words. The STATUS of the response
+ * is set by the framework and is RPMI_SUCCESS whenever OP-TEE was reached,
+ * errors reported by OP-TEE itself are returned in a0 as usual.
+ */
+
+/*
+ * UUID identifying the OP-TEE API as a TEE_CALL service, the same value
+ * as returned by OPTEE_SMC_CALLS_UID (OPTEE_MSG_UID_0..3).
+ */
+#define OPTEE_RPMI_SERVICE_UUID \
+ UUID_INIT(0x384fb3e0, 0xe7f8, 0x11e3, \
+ 0xaf, 0x63, 0x00, 0x02, 0xa5, 0xd5, 0xc5, 0x1b)
+
+#define OPTEE_RPMI_CALL_NUM_ARGS 8
+#define OPTEE_RPMI_CALL_NUM_RETS 4
+
+/**
+ * struct optee_rpmi_call_req - TEE_CALL request data invoking the SMC ABI
+ * @sender_id: SENDER_ID, endpoint identifier of the REE
+ * @target_id: TARGET_ID, endpoint identifier of OP-TEE
+ * @service: SERVICE, bytes of OPTEE_RPMI_SERVICE_UUID
+ * @data_len: SERVICE_DATA_LEN, sizeof(@args)
+ * @args: SERVICE_DATA, register arguments a0-a7 of the SMC ABI
+ */
+struct optee_rpmi_call_req {
+ __le32 sender_id;
+ __le32 target_id;
+ u8 service[UUID_SIZE];
+ __le32 data_len;
+ __le64 args[OPTEE_RPMI_CALL_NUM_ARGS];
+} __packed;
+
+/**
+ * struct optee_rpmi_call_rsp - TEE_CALL response data of the SMC ABI
+ * @status: STATUS, RPMI error code set by the framework
+ * @rsp_len: SERVICE_RSP_LEN, sizeof(@rets)
+ * @rets: SERVICE_RSP, return values a0-a3 of the SMC ABI
+ */
+struct optee_rpmi_call_rsp {
+ __le32 status;
+ __le32 rsp_len;
+ __le64 rets[OPTEE_RPMI_CALL_NUM_RETS];
+} __packed;
+
+#endif /*OPTEE_RPMI_H*/
diff --git a/drivers/tee/optee/rpmi_conduit.c b/drivers/tee/optee/rpmi_conduit.c
new file mode 100644
index 000000000..bc4ff975c
--- /dev/null
+++ b/drivers/tee/optee/rpmi_conduit.c
@@ -0,0 +1,203 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright 2026 NXP
+ */
+
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/arm-smccc.h>
+#include <linux/build_bug.h>
+#include <linux/device.h>
+#include <linux/err.h>
+#include <linux/errno.h>
+#include <linux/mailbox_client.h>
+#include <linux/mailbox/riscv-rpmi-message.h>
+#include <linux/mailbox/riscv-sbi-mpxy-mbox.h>
+#include <linux/printk.h>
+#include <linux/property.h>
+#include <linux/types.h>
+#include <linux/uuid.h>
+#include "optee_private.h"
+#include "optee_rpmi.h"
+#include "optee_smc.h"
+
+/*
+ * This file implements the conduit carrying the SMC ABI over the TEE
+ * service group of the RISC-V Platform Management Interface (RPMI). Each
+ * invocation of the ABI is a TEE_CALL request sent on an SBI MPXY channel
+ * with the message layout described in optee_rpmi.h.
+ *
+ * The TEE_CALL request is not sent with mbox_send_message(): the target
+ * TEE executes on the calling hart until it responds, so the transfer is
+ * done with riscv_sbi_mpxy_mbox_call() which bypasses the mailbox core
+ * queue and channel lock. Calls from different harts run concurrently,
+ * exactly like SMCs do.
+ */
+
+static_assert(sizeof(struct optee_rpmi_call_req) == 92);
+static_assert(sizeof(struct optee_rpmi_call_rsp) == 40);
+
+/**
+ * struct optee_rpmi_conduit - RPMI TEE service group conduit
+ * @cl: mailbox client owning @chan
+ * @chan: SBI MPXY channel implementing the RPMI TEE service group
+ * @sender_id: endpoint identifier of the REE
+ * @target_id: endpoint identifier of OP-TEE
+ */
+struct optee_rpmi_conduit {
+ struct mbox_client cl;
+ struct mbox_chan *chan;
+ u32 sender_id;
+ u32 target_id;
+};
+
+/* Like the rest of the SMC ABI, at most a single OP-TEE instance is supported */
+static struct optee_rpmi_conduit optee_rpmi;
+
+static const uuid_t optee_rpmi_service_uuid = OPTEE_RPMI_SERVICE_UUID;
+
+static void optee_rpmi_invoke_fn(unsigned long a0, unsigned long a1,
+ unsigned long a2, unsigned long a3,
+ unsigned long a4, unsigned long a5,
+ unsigned long a6, unsigned long a7,
+ struct arm_smccc_res *res)
+{
+ struct optee_rpmi_call_req req = {
+ .sender_id = cpu_to_le32(optee_rpmi.sender_id),
+ .target_id = cpu_to_le32(optee_rpmi.target_id),
+ .data_len = cpu_to_le32(sizeof(req.args)),
+ .args = {
+ cpu_to_le64(a0), cpu_to_le64(a1), cpu_to_le64(a2),
+ cpu_to_le64(a3), cpu_to_le64(a4), cpu_to_le64(a5),
+ cpu_to_le64(a6), cpu_to_le64(a7),
+ },
+ };
+ struct optee_rpmi_call_rsp rsp = {};
+ struct rpmi_mbox_message msg;
+ int rc;
+
+ export_uuid(req.service, &optee_rpmi_service_uuid);
+
+ rpmi_mbox_init_send_with_response(&msg, RPMI_TEE_SRV_CALL,
+ &req, sizeof(req), &rsp, sizeof(rsp));
+ rc = riscv_sbi_mpxy_mbox_call(optee_rpmi.chan, &msg);
+ if (!rc) {
+ if (msg.data.out_response_len < sizeof(rsp.status))
+ rc = -EIO;
+ else
+ rc = rpmi_to_linux_error((s32)le32_to_cpu(rsp.status));
+ }
+ if (!rc && (msg.data.out_response_len < sizeof(rsp) ||
+ le32_to_cpu(rsp.rsp_len) != sizeof(rsp.rets)))
+ rc = -EIO;
+
+ if (rc) {
+ /*
+ * The SBI implementation or the RPMI TEE framework failed to
+ * deliver the call, OP-TEE was not reached. Report it in a0
+ * as the SMC ABI does for a call that could not be handled.
+ */
+ if (rc != -EBUSY)
+ pr_warn_ratelimited("TEE_CALL of 0x%lx failed: %d\n",
+ a0, rc);
+ res->a0 = rc == -EBUSY ? OPTEE_SMC_RETURN_EBUSY :
+ OPTEE_SMC_RETURN_ENOTAVAIL;
+ res->a1 = 0;
+ res->a2 = 0;
+ res->a3 = 0;
+ return;
+ }
+
+ res->a0 = le64_to_cpu(rsp.rets[0]);
+ res->a1 = le64_to_cpu(rsp.rets[1]);
+ res->a2 = le64_to_cpu(rsp.rets[2]);
+ res->a3 = le64_to_cpu(rsp.rets[3]);
+}
+
+static void optee_rpmi_conduit_release(void *data)
+{
+ struct optee_rpmi_conduit *conduit = data;
+
+ mbox_free_channel(conduit->chan);
+ conduit->chan = NULL;
+}
+
+static int optee_rpmi_get_attr(struct optee_rpmi_conduit *conduit,
+ enum rpmi_mbox_attribute_id id, u32 *value)
+{
+ struct rpmi_mbox_message msg;
+ int rc;
+
+ rpmi_mbox_init_get_attribute(&msg, id);
+ rc = rpmi_mbox_send_message(conduit->chan, &msg);
+ if (rc)
+ return rc;
+
+ *value = msg.attr.value;
+ return 0;
+}
+
+optee_invoke_fn *optee_rpmi_conduit_init(struct device *dev)
+{
+ struct optee_rpmi_conduit *conduit = &optee_rpmi;
+ u32 value;
+ int rc;
+
+ if (conduit->chan)
+ return ERR_PTR(-EBUSY);
+
+ rc = device_property_read_u32(dev, "riscv,rpmi-tee-sender-id",
+ &conduit->sender_id);
+ if (rc)
+ return ERR_PTR(dev_err_probe(dev, rc,
+ "missing \"riscv,rpmi-tee-sender-id\" property\n"));
+
+ rc = device_property_read_u32(dev, "riscv,rpmi-tee-target-id",
+ &conduit->target_id);
+ if (rc)
+ return ERR_PTR(dev_err_probe(dev, rc,
+ "missing \"riscv,rpmi-tee-target-id\" property\n"));
+
+ conduit->cl.dev = dev;
+ conduit->cl.tx_block = false;
+ conduit->cl.knows_txdone = true;
+ conduit->chan = mbox_request_channel(&conduit->cl, 0);
+ if (IS_ERR(conduit->chan)) {
+ rc = PTR_ERR(conduit->chan);
+ conduit->chan = NULL;
+ return ERR_PTR(dev_err_probe(dev, rc,
+ "failed to request MPXY channel\n"));
+ }
+
+ rc = devm_add_action_or_reset(dev, optee_rpmi_conduit_release, conduit);
+ if (rc)
+ return ERR_PTR(rc);
+
+ rc = optee_rpmi_get_attr(conduit, RPMI_MBOX_ATTR_SERVICEGROUP_ID,
+ &value);
+ if (rc)
+ return ERR_PTR(dev_err_probe(dev, rc,
+ "failed to read RPMI service group ID\n"));
+ if (value != RPMI_SRVGRP_TEE) {
+ dev_err(dev, "MPXY channel implements RPMI service group 0x%x, not TEE\n",
+ value);
+ return ERR_PTR(-ENODEV);
+ }
+
+ rc = optee_rpmi_get_attr(conduit, RPMI_MBOX_ATTR_MAX_MSG_DATA_SIZE,
+ &value);
+ if (rc)
+ return ERR_PTR(dev_err_probe(dev, rc,
+ "failed to read RPMI max message size\n"));
+ if (value < sizeof(struct optee_rpmi_call_req) ||
+ value < sizeof(struct optee_rpmi_call_rsp)) {
+ dev_err(dev, "MPXY channel message size %u is too small\n",
+ value);
+ return ERR_PTR(-EINVAL);
+ }
+
+ pr_info("using RPMI TEE service group, endpoints %u -> %u\n",
+ conduit->sender_id, conduit->target_id);
+
+ return optee_rpmi_invoke_fn;
+}
diff --git a/drivers/tee/optee/smc_abi.c b/drivers/tee/optee/smc_abi.c
index 221939f72..3d5d54526 100644
--- a/drivers/tee/optee/smc_abi.c
+++ b/drivers/tee/optee/smc_abi.c
@@ -1522,6 +1522,12 @@ static const struct optee_smc_conduit optee_smccc_conduit = {
};
#endif

+#if IS_ENABLED(CONFIG_OPTEE_RPMI_CONDUIT)
+static const struct optee_smc_conduit optee_rpmi_conduit = {
+ .init = optee_rpmi_conduit_init,
+};
+#endif
+
/* optee_remove - Device Removal Routine
* @pdev: platform device information struct
*
@@ -1982,6 +1988,9 @@ static int optee_probe(struct platform_device *pdev)
static const struct of_device_id optee_dt_match[] = {
#ifdef CONFIG_HAVE_ARM_SMCCC
{ .compatible = "linaro,optee-tz", .data = &optee_smccc_conduit },
+#endif
+#if IS_ENABLED(CONFIG_OPTEE_RPMI_CONDUIT)
+ { .compatible = "linaro,optee-rpmi", .data = &optee_rpmi_conduit },
#endif
{},
};
--
2.43.0