Re: [PATCH v4 3/4] firmware: qcom_scm: Add support for Qualcomm Secure Execution Environment SCM interface

From: Johan Hovold
Date: Wed Jun 28 2023 - 08:11:17 EST


On Mon, May 29, 2023 at 01:03:50AM +0200, Maximilian Luz wrote:
> Add support for SCM calls to Secure OS and the Secure Execution
> Environment (SEE) residing in the TrustZone (TZ) via the QSEECOM
> interface. This allows communication with Secure/TZ applications, for
> example 'uefisecapp' managing access to UEFI variables.
>
> The added interface attempts to automatically detect known and supported
> applications, creating a client (auxiliary) device for each one. The
> respective client/auxiliary driver is then responsible for managing and
> communicating with the application.
>
> While this patch introduces only a very basic interface without the more
> advanced features (such as re-entrant and blocking SCM calls and
> listeners/callbacks), this is enough to talk to the aforementioned
> 'uefisecapp'.
>
> Signed-off-by: Maximilian Luz <luzmaximilian@xxxxxxxxx>
> ---
>
> Changes in v4:
> - Remove instantiation of dedicated QSEECOM device and load the driver
> via qcom_scm instead. In particular:
> - Add a list of tested devices to ensure that we don't run into any
> issues with the currently unimplemented re-entrant calls.
> - Use the QSEECOM version to check for general availability of the
> interface.
> - Attempt to automatically detect available QSEECOM applications
> (and instantiate respective clients) based on a fixed list.
> - Use auxiliary bus and devices for clients instead of MFD.
> - Restructure DMA allocations: Use dma_map_single() directly inside
> qcom_scm_qseecom_app_send() instead of requiring clients to allocate
> DMA memory themselves.

> +#ifdef CONFIG_QCOM_SCM_QSEECOM
> +
> +/* Lock for QSEECOM SCM call executions */
> +DEFINE_MUTEX(qcom_scm_qseecom_call_lock);

Missing static keyword.

> +/**
> + * qcom_scm_qseecom_call() - Perform a QSEECOM SCM call.
> + * @desc: SCM call descriptor.
> + * @res: SCM call response (output).
> + *
> + * Performs the QSEECOM SCM call described by @desc, returning the response in
> + * @rsp.
> + *
> + * Return: Returns zero on success, nonzero on failure.

Nit: You should drop "Returns" here and capitalise Zero. Similar below.

> + */

> +/**
> + * qcom_scm_qseecom_get_version() - Query the QSEECOM version.
> + * @version: Pointer where the QSEECOM version will be stored.
> + *
> + * Performs the QSEECOM SCM querying the QSEECOM version currently running in
> + * the TrustZone.
> + *
> + * Return: Returns zero on success, nonzero on failure.
> + */
> +static int qcom_scm_qseecom_get_version(u32 *version)
> +{
> + struct qcom_scm_desc desc = {};
> + struct qcom_scm_qseecom_resp res = {};
> + u32 feature = 10;
> + int ret;
> +
> + desc.owner = QSEECOM_TZ_OWNER_SIP;
> + desc.svc = QSEECOM_TZ_SVC_INFO;
> + desc.cmd = 0x03;

I know this has been reverse engineered, but is it possible to name also
these cmd codes?

> + desc.arginfo = QCOM_SCM_ARGS(1, QCOM_SCM_VAL);
> + desc.args[0] = feature;
> +
> + ret = qcom_scm_qseecom_call(&desc, &res);
> + if (ret)
> + return ret;
> +
> + *version = res.result;
> + return 0;
> +}
> +
> +/**
> + * qcom_scm_qseecom_app_get_id() - Query the app ID for a given QSEE app name.
> + * @app_name: The name of the app.
> + * @app_id: The returned app ID.
> + *
> + * Query and return the application ID of the SEE app identified by the given
> + * name. This returned ID is the unique identifier of the app required for
> + * subsequent communication.
> + *
> + * Return: Returns zero on success, nonzero on failure. Returns -ENOENT if the
> + * app has not been loaded or could not be found.
> + */
> +static int qcom_scm_qseecom_app_get_id(const char *app_name, u32 *app_id)
> +{
> + unsigned long name_buf_size = QSEECOM_MAX_APP_NAME_SIZE;
> + unsigned long app_name_len = strlen(app_name);
> + struct qcom_scm_desc desc = {};
> + struct qcom_scm_qseecom_resp res = {};
> + dma_addr_t name_buf_phys;
> + char *name_buf;
> + int status;
> +
> + if (app_name_len >= name_buf_size)
> + return -EINVAL;
> +
> + name_buf = kzalloc(name_buf_size, GFP_KERNEL);
> + if (!name_buf)
> + return -ENOMEM;
> +
> + memcpy(name_buf, app_name, app_name_len);
> +
> + name_buf_phys = dma_map_single(__scm->dev, name_buf, name_buf_size, DMA_TO_DEVICE);
> + if (dma_mapping_error(__scm->dev, name_buf_phys)) {
> + kfree(name_buf);
> + dev_err(__scm->dev, "qseecom: failed to map dma address\n");
> + return -EFAULT;

This should be -ENOMEM (you can also just use the return value from
dma_mapping_error()). Similar below.

> + }
> +
> + desc.owner = QSEECOM_TZ_OWNER_QSEE_OS;
> + desc.svc = QSEECOM_TZ_SVC_APP_MGR;
> + desc.cmd = 0x03;
> + desc.arginfo = QCOM_SCM_ARGS(2, QCOM_SCM_RW, QCOM_SCM_VAL);
> + desc.args[0] = name_buf_phys;
> + desc.args[1] = app_name_len;
> +
> + status = qcom_scm_qseecom_call(&desc, &res);
> + dma_unmap_single(__scm->dev, name_buf_phys, name_buf_size, DMA_TO_DEVICE);
> + kfree(name_buf);
> +
> + if (status)
> + return status;
> +
> + if (res.result == QSEECOM_RESULT_FAILURE)
> + return -ENOENT;
> +
> + if (res.result != QSEECOM_RESULT_SUCCESS)
> + return -EINVAL;
> +
> + if (res.resp_type != QSEECOM_SCM_RES_APP_ID)
> + return -EINVAL;
> +
> + *app_id = res.data;
> + return 0;
> +}
> +
> +/**
> + * qcom_scm_qseecom_app_send() - Send to and receive data from a given QSEE app.
> + * @client: The QSEECOM client device corresponding to the target app.
> + * @req: Request buffer sent to the app (must be DMA-mappable).
> + * @req_size: Size of the request buffer.
> + * @rsp: Response buffer, written to by the app (must be DMA-mappable).
> + * @rsp_size: Size of the response buffer.
> + *
> + * Sends a request to the QSEE app associated with the given client and read
> + * back its response. The caller must provide two DMA memory regions, one for
> + * the request and one for the response, and fill out the @req region with the
> + * respective (app-specific) request data. The QSEE app reads this and returns
> + * its response in the @rsp region.
> + *
> + * Return: Returns zero on success, nonzero error code on failure.
> + */
> +int qcom_scm_qseecom_app_send(struct qseecom_client *client, void *req,
> + size_t req_size, void *rsp, size_t rsp_size)
> +{

> +}
> +EXPORT_SYMBOL(qcom_scm_qseecom_app_send);

Should this not be EXPORT_SYMBOL_GPL()?

> +
> +static void qseecom_client_release(struct device *dev)
> +{
> + struct qseecom_client *client = container_of(dev, struct qseecom_client, aux_dev.dev);
> +
> + kfree(client);
> +}
> +
> +static void qseecom_client_remove(void *data)
> +{
> + struct qseecom_client *client = data;
> +
> + auxiliary_device_delete(&client->aux_dev);
> + auxiliary_device_uninit(&client->aux_dev);
> +}
> +
> +static int qseecom_client_register(const struct qseecom_app_desc *desc)
> +{
> + struct qseecom_client *client;
> + u32 app_id;
> + int ret;
> +
> + /* Try to find the app ID, skip device if not found */
> + ret = qcom_scm_qseecom_app_get_id(desc->app_name, &app_id);
> + if (ret)
> + return ret == -ENOENT ? 0 : ret;
> +
> + dev_info(__scm->dev, "qseecom: setting up client for %s\n", desc->app_name);
> +
> + /* Allocate and set-up the client device */
> + client = kzalloc(sizeof(*client), GFP_KERNEL);
> + if (!client)
> + return -ENOMEM;
> +
> + client->aux_dev.name = desc->dev_name;
> + client->aux_dev.dev.parent = __scm->dev;
> + client->aux_dev.dev.release = qseecom_client_release;
> + client->app_id = app_id;
> +
> + ret = auxiliary_device_init(&client->aux_dev);
> + if (ret) {
> + kfree(client);
> + return ret;
> + }
> +
> + ret = auxiliary_device_add(&client->aux_dev);
> + if (ret) {
> + auxiliary_device_uninit(&client->aux_dev);
> + return ret;
> + }
> +
> + /*
> + * Ensure that the device is properly cleaned up in case of removal or
> + * errors.
> + */
> + ret = devm_add_action_or_reset(__scm->dev, qseecom_client_remove, client);
> + if (ret)
> + return ret;
> +
> + return 0;
> +}
> +
> +/*
> + * We do not yet support re-entrant calls via the qseecom interface. To prevent
> + + any potential issues with this, only allow validated devices for now.
> + */
> +static const struct of_device_id qcom_scm_qseecom_allowlist[] = {
> + { .compatible = "lenovo,thinkpad-x13s", },
> + { }
> +};
> +
> +static bool qcom_scm_qseecom_device_allowed(void)
> +{
> + struct device_node *np;
> + bool match;
> +
> + np = of_find_node_by_path("/");
> + match = of_match_node(qcom_scm_qseecom_allowlist, np);
> + of_node_put(np);
> +
> + return match;
> +}
> +
> +static const struct qseecom_app_desc qcom_scm_qseecom_apps[] = {};
> +
> +static int qcom_scm_qseecom_init(void)
> +{
> + u32 version;
> + int ret, i;
> +
> + /* Try to query the qseecom version, skip qseecom setup if this fails */
> + ret = qcom_scm_qseecom_get_version(&version);
> + if (ret)
> + return 0;
> +
> + dev_info(__scm->dev, "qseecom: found qseecom with version 0x%x\n", version);
> +
> + /* Check against tested/verified devices */
> + if (!qcom_scm_qseecom_device_allowed()) {
> + dev_info(__scm->dev, "qseecom: untested device, skipping\n");

Perhaps call the helper machine_allowed() and update the commit message
to match (cf. of_machine_is_compatible())?

> + return 0;
> + }
> +
> + /* Set up client devices for each base application */
> + for (i = 0; i < ARRAY_SIZE(qcom_scm_qseecom_apps); i++) {
> + ret = qseecom_client_register(&qcom_scm_qseecom_apps[i]);
> + if (ret)
> + return ret;
> + }
> +
> + return 0;
> +}
> +
> +#else /* CONFIG_QCOM_SCM_QSEECOM */
> +
> +static int qcom_scm_qseecom_init(void)
> +{
> + return 0;
> +}
> +
> +#endif /* CONFIG_QCOM_SCM_QSEECOM */
> +
> /**
> * qcom_scm_is_available() - Checks if SCM is available
> */
> @@ -1496,6 +1903,12 @@ static int qcom_scm_probe(struct platform_device *pdev)
>
> __get_convention();
>
> + ret = qcom_scm_qseecom_init();
> + if (ret < 0) {
> + __scm = NULL;

So as I mentioned in my reply to 2/4, you can still have clients
registered here when you clear the __scm pointer which they rely on
after an error.

Not sure how best to handle this, but perhaps registering a qseecom
platform device here and have it's driver probe defer until scm is
available would work?

That way you could also separate out the qseecom implementation in a
separate file (driver) rather than having the ifdef above.

> + return dev_err_probe(scm->dev, ret, "Failed to initialize qseecom\n");
> + }
> +
> /*
> * If requested enable "download mode", from this point on warmboot
> * will cause the boot stages to enter download mode, unless

Overall this looks really good. Nice job!

Johan