[RFC PATCH 1/2] firmware: qcom: scm: Add QSEECOM app loading and shutdown
From: Haozhen Li
Date: Sun Nov 01 2026 - 11:01:46 EST
QSEECOM clients in the kernel can currently only talk to trusted
applications that some earlier boot stage happened to load:
qcom_scm_qseecom_app_get_id() looks an app up and
qcom_scm_qseecom_app_send() talks to it, but nothing lets Linux start
one itself.
Add the two missing app-manager commands:
- qcom_scm_qseecom_app_load() authenticates, loads and starts an app
from an image staged in TZ memory, handing it over with the
APP_START call. Both image layouts QTEE accepts are supported: a
split image (.mdt metadata followed by the segment files, with the
metadata length passed as mdt_len) and a single-file signed ELF
(mdt_len == 0), where QTEE parses the metadata from the program
headers itself.
- qcom_scm_qseecom_app_shutdown() stops an app and releases its
secure-world memory and name. Without it an app can only be loaded
once per boot: QTEE refuses a second load, and only a reboot
clears the name.
QTEE reads the image address passed to APP_START as a 32-bit value
even under the 64-bit SMC calling convention, and truncates anything
above 4 GiB. Catch that case in qcom_scm_qseecom_app_load() and fail
with -EINVAL rather than pass an address on for silent truncation: on
platforms whose firmware hands out memory above 4 GiB the load would
otherwise fail far from the cause, or worse.
Based on the QSEECOM transport work of Dawid Wrobel and samcday
(https://github.com/samcday/linux).
Signed-off-by: Haozhen Li <dezige131@xxxxxxxxxxx>
---
drivers/firmware/qcom/qcom_scm.c | 123 +++++++++++++++++++++++++
include/linux/firmware/qcom/qcom_scm.h | 14 +++
2 files changed, 137 insertions(+)
diff --git a/drivers/firmware/qcom/qcom_scm.c b/drivers/firmware/qcom/qcom_scm.c
index 43e250fae..c77130693 100644
--- a/drivers/firmware/qcom/qcom_scm.c
+++ b/drivers/firmware/qcom/qcom_scm.c
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-2.0-only
/* Copyright (c) 2010,2015,2019 The Linux Foundation. All rights reserved.
* Copyright (C) 2015 Linaro Ltd.
+ * Copyright (C) 2026 Dawid Wróbel <me@xxxxxxxxxxxxxxx>
*/
#include <linux/arm-smccc.h>
@@ -115,7 +116,14 @@ enum qcom_scm_qseecom_tz_svc {
};
enum qcom_scm_qseecom_tz_cmd_app {
+ /*
+ * APP_START and APP_SEND share the same command number and are
+ * told apart by owner and service: APP_START is
+ * QSEE_OS/APP_MGR, APP_SEND is TZ_APPS/APP_ID_PLACEHOLDER.
+ */
+ QSEECOM_TZ_CMD_APP_START = 1,
QSEECOM_TZ_CMD_APP_SEND = 1,
+ QSEECOM_TZ_CMD_APP_SHUTDOWN = 2,
QSEECOM_TZ_CMD_APP_LOOKUP = 3,
};
@@ -2168,6 +2176,121 @@ int qcom_scm_qseecom_app_get_id(const char *app_name, u32 *app_id)
}
EXPORT_SYMBOL_GPL(qcom_scm_qseecom_app_get_id);
+/**
+ * qcom_scm_qseecom_app_load() - Load and start a QSEE application.
+ * @img: Image buffer, must be allocated from TZ memory.
+ * @mdt_len: Length of the .mdt metadata at the start of @img, or zero if
+ * @img is a single-file image.
+ * @img_len: Total length of the image in bytes.
+ * @app_id: ID assigned to the loaded application.
+ *
+ * Hands an application image to QTEE for authentication, loading and
+ * starting, and returns the ID under which qcom_scm_qseecom_app_send()
+ * reaches it.
+ *
+ * Two image layouts are accepted. With a non-zero @mdt_len, @img holds the
+ * .mdt metadata followed by each segment file concatenated in
+ * program-header order; in that layout the metadata segments appear twice,
+ * as the .mdt already contains them, and padding between segments is
+ * dropped. With @mdt_len == 0, @img is a single signed ELF passed as one
+ * file, and QTEE parses the metadata from the program headers itself.
+ *
+ * The image buffer must lie entirely below the 4 GiB physical mark: QTEE
+ * reads the image address as a 32-bit value even under the 64-bit SMC
+ * calling convention, so a buffer above it would be truncated into a wrong
+ * address. Such buffers are rejected here rather than passed on.
+ *
+ * Return: Zero on success, nonzero on failure.
+ */
+int qcom_scm_qseecom_app_load(void *img, size_t mdt_len, size_t img_len,
+ u32 *app_id)
+{
+ phys_addr_t img_phys;
+ struct qcom_scm_qseecom_resp res = {};
+ struct qcom_scm_desc desc = {};
+ int status;
+
+ if (!img || !img_len || mdt_len > img_len)
+ return -EINVAL;
+
+ /*
+ * Unlike the lookup command, which passes a buffer holding the name,
+ * the image is handed over as a bare physical address: all three
+ * arguments are plain values.
+ */
+ desc.owner = QSEECOM_TZ_OWNER_QSEE_OS;
+ desc.svc = QSEECOM_TZ_SVC_APP_MGR;
+ desc.cmd = QSEECOM_TZ_CMD_APP_START;
+ desc.arginfo = QCOM_SCM_ARGS(3);
+ desc.args[0] = mdt_len;
+ desc.args[1] = img_len;
+
+ img_phys = qcom_tzmem_to_phys(img);
+ if (!img_phys)
+ return -EINVAL;
+
+ /* See the comment on the 4 GiB limit in the function doc above. */
+ if (upper_32_bits(img_phys) || (u64)img_phys + img_len > SZ_4G) {
+ dev_err(__scm->dev, "qseecom: image buffer beyond 4GB: %pap\n", &img_phys);
+ return -EINVAL;
+ }
+
+ desc.args[2] = img_phys;
+
+ status = qcom_scm_qseecom_call(&desc, &res);
+ if (status)
+ return status;
+
+ if (res.result != QSEECOM_RESULT_SUCCESS) {
+ dev_dbg(__scm->dev,
+ "qseecom: app load rejected: result %llu, type %llu, data %llu\n",
+ res.result, res.resp_type, res.data);
+ return -EIO;
+ }
+
+ if (res.resp_type != QSEECOM_SCM_RES_APP_ID)
+ return -EINVAL;
+
+ *app_id = res.data;
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(qcom_scm_qseecom_app_load);
+
+/**
+ * qcom_scm_qseecom_app_shutdown() - Stop and unload a QSEE application.
+ * @app_id: The ID of the application to unload.
+ *
+ * The counterpart to qcom_scm_qseecom_app_load(): stops the application
+ * and releases the secure-world memory it occupies, freeing its name for a
+ * later load. Without this an application can be loaded only once per
+ * boot: QTEE refuses a second load, and only a reboot clears the name.
+ *
+ * Return: Zero on success, nonzero on failure.
+ */
+int qcom_scm_qseecom_app_shutdown(u32 app_id)
+{
+ struct qcom_scm_qseecom_resp res = {};
+ struct qcom_scm_desc desc = {};
+ int status;
+
+ desc.owner = QSEECOM_TZ_OWNER_QSEE_OS;
+ desc.svc = QSEECOM_TZ_SVC_APP_MGR;
+ desc.cmd = QSEECOM_TZ_CMD_APP_SHUTDOWN;
+ desc.arginfo = QCOM_SCM_ARGS(1);
+ desc.args[0] = app_id;
+
+ status = qcom_scm_qseecom_call(&desc, &res);
+ if (status)
+ return status;
+
+ if (res.result != QSEECOM_RESULT_SUCCESS)
+ return -EINVAL;
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(qcom_scm_qseecom_app_shutdown);
+
/**
* qcom_scm_qseecom_app_send() - Send to and receive data from a given QSEE app.
* @app_id: The ID of the target app.
diff --git a/include/linux/firmware/qcom/qcom_scm.h b/include/linux/firmware/qcom/qcom_scm.h
index a0a6bc022..8f98b57e1 100644
--- a/include/linux/firmware/qcom/qcom_scm.h
+++ b/include/linux/firmware/qcom/qcom_scm.h
@@ -141,6 +141,9 @@ int qcom_scm_shm_bridge_delete(u64 handle);
#ifdef CONFIG_QCOM_QSEECOM
int qcom_scm_qseecom_app_get_id(const char *app_name, u32 *app_id);
+int qcom_scm_qseecom_app_load(void *img, size_t mdt_len, size_t img_len,
+ u32 *app_id);
+int qcom_scm_qseecom_app_shutdown(u32 app_id);
int qcom_scm_qseecom_app_send(u32 app_id, void *req, size_t req_size,
void *rsp, size_t rsp_size);
@@ -151,6 +154,17 @@ static inline int qcom_scm_qseecom_app_get_id(const char *app_name, u32 *app_id)
return -EINVAL;
}
+static inline int qcom_scm_qseecom_app_load(void *img, size_t mdt_len,
+ size_t img_len, u32 *app_id)
+{
+ return -EINVAL;
+}
+
+static inline int qcom_scm_qseecom_app_shutdown(u32 app_id)
+{
+ return -EINVAL;
+}
+
static inline int qcom_scm_qseecom_app_send(u32 app_id,
void *req, size_t req_size,
void *rsp, size_t rsp_size)
--
2.43.0