Re: [PATCH v2 2/2] firmware: socfpga-fcs: add Altera SoCFPGA FCS driver with SDOS
From: Hang Suan Wang
Date: Thu Jul 30 2026 - 12:46:26 EST
Response based on Sashiko review:
https://sashiko.dev/#/patchset/cover.1783966717.git.hang.suan.wang%40altera.com
On 14/7/2026 2:35 am, hang.suan.wang@xxxxxxxxxx wrote:
> From: Hang Suan Wang <hang.suan.wang@xxxxxxxxxx>
>
> Add the Altera SoCFPGA Crypto Service (FCS) driver, which exposes the
> Secure Data Object Service (SDOS) encrypt/decrypt operation and its crypto
> session lifecycle to non-secure host software.
>
> The SDOS is the FCS feature that protects data at rest: the SDM encrypts
> and decrypts using a key derived from a device-unique SDOS root key plus an
> SDM-generated IV, so the host never handles raw key material or IVs. It
> only submits plaintext it already owns and receives authenticated
> ciphertext objects managed by the SDM. A primary use case is black key
> provisioning, where operational keys are installed without ever appearing
> in cleartext.
>
> The driver is a standalone module and describes no hardware of its own.
> It binds by name to the "stratix10-fcs" platform device that the
> stratix10-svc driver registers in code, so no device-tree node is needed,
> and detects the SoC by matching the service-layer compatible. It exposes
> the following sysfs attributes. The SDOS requests are issued to the SDM
> through the stratix10-svc asynchronous SIP SMC. Source and destination
> buffers are taken from the service-layer memory pool so the SDM can reach
> them via physical or SMMU-remapped addresses.
>
> For encryption the SDM returns a structured object (metadata, IV, HMAC,
> ciphertext); for decryption it validates the HMAC, recovers the parameters
> from the object header, and enforces a 64-bit owner ID so that only the
> creator of an object can decrypt it.
>
> Signed-off-by: Hang Suan Wang <hang.suan.wang@xxxxxxxxxx>
> Reviewed-by: Dinh Nguyen <dinguyen@xxxxxxxxxx>
> ---
> MAINTAINERS | 8 +
> drivers/firmware/Kconfig | 16 +
> drivers/firmware/Makefile | 2 +
> drivers/firmware/socfpga-fcs-core.c | 640 +++++++++++++++++++++
> drivers/firmware/socfpga-fcs.c | 250 ++++++++
> include/linux/firmware/intel/socfpga-fcs.h | 133 +++++
> 6 files changed, 1049 insertions(+)
> create mode 100644 drivers/firmware/socfpga-fcs-core.c
> create mode 100644 drivers/firmware/socfpga-fcs.c
> create mode 100644 include/linux/firmware/intel/socfpga-fcs.h
>
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 15011f5752a9..72b12b32f8fe 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -946,6 +946,14 @@ ALPS PS/2 TOUCHPAD DRIVER
> R: Pali Rohár <pali@xxxxxxxxxx>
> F: drivers/input/mouse/alps.*
>
> +ALTERA FCS DRIVER
> +M: Hang Suan Wang <hang.suan.wang@xxxxxxxxxx>
> +M: Genevieve Chan <genevieve.chan@xxxxxxxxxx>
> +L: linux-arm-kernel@xxxxxxxxxxxxxxxxxxx
> +S: Maintained
> +F: drivers/firmware/socfpga-fcs*
> +F: include/linux/firmware/intel/socfpga-fcs*
> +
> ALTERA MAILBOX DRIVER
> M: Tien Sung Ang <tiensung.ang@xxxxxxxxxx>
> S: Maintained
> diff --git a/drivers/firmware/Kconfig b/drivers/firmware/Kconfig
> index 12dc70254842..9a70def6932a 100644
> --- a/drivers/firmware/Kconfig
> +++ b/drivers/firmware/Kconfig
> @@ -172,6 +172,22 @@ config INTEL_STRATIX10_RSU
>
> Say Y here if you want Intel RSU support.
>
> +config ALTERA_SOCFPGA_FCS
> + tristate "Altera SoCFPGA Crypto Service (FCS) configuration"
> + depends on INTEL_STRATIX10_SERVICE
> + default n
> + help
> + Altera SoCFPGA Crypto Service (FCS) driver exposes interfaces access
> + through the Intel Service Layer to user space via sysfs device
> + attribute nodes. It exposes the crypto and key-management services
> + of the Secure Device Manager (SDM) to the host software stack and
> + requests are forwarded to Arm Trusted Firmware. The SDM then
> + executes or authorizes them using device-rooted security resources.
> + Protected key material remains within the secure firmware boundary
> + and is not directly exposed to non-secure host software.
> +
> + Say Y here if you want Altera SoCFPGA FCS support.
> +
> config MTK_ADSP_IPC
> tristate "MTK ADSP IPC Protocol driver"
> depends on MTK_ADSP_MBOX
> diff --git a/drivers/firmware/Makefile b/drivers/firmware/Makefile
> index 4ddec2820c96..e9f52f0e5f7a 100644
> --- a/drivers/firmware/Makefile
> +++ b/drivers/firmware/Makefile
> @@ -10,6 +10,8 @@ obj-$(CONFIG_EDD) += edd.o
> obj-$(CONFIG_DMIID) += dmi-id.o
> obj-$(CONFIG_INTEL_STRATIX10_SERVICE) += stratix10-svc.o
> obj-$(CONFIG_INTEL_STRATIX10_RSU) += stratix10-rsu.o
> +obj-$(CONFIG_ALTERA_SOCFPGA_FCS) += altera-fcs.o
> +altera-fcs-y := socfpga-fcs.o socfpga-fcs-core.o
> obj-$(CONFIG_ISCSI_IBFT_FIND) += iscsi_ibft_find.o
> obj-$(CONFIG_ISCSI_IBFT) += iscsi_ibft.o
> obj-$(CONFIG_FIRMWARE_MEMMAP) += memmap.o
> diff --git a/drivers/firmware/socfpga-fcs-core.c b/drivers/firmware/socfpga-fcs-core.c
> new file mode 100644
> index 000000000000..598a4d2e3c5b
> --- /dev/null
> +++ b/drivers/firmware/socfpga-fcs-core.c
> @@ -0,0 +1,640 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/*
> + * Copyright (C) 2026 Altera Corporation
> + */
> +
> +#include <linux/delay.h>
> +#include <linux/of.h>
> +#include <linux/slab.h>
> +#include <linux/uaccess.h>
> +#include <linux/firmware/intel/socfpga-fcs.h>
> +#include <linux/firmware/intel/stratix10-svc-client.h>
> +
> +#define OWNER_ID_OFFSET 12
> +#define OWNER_ID_SIZE 8
> +
> +#define SDOS_DECRYPTION_REPROVISION_KEY_WARN 0x102
> +#define SDOS_DECRYPTION_NOT_LATEST_KEY_WARN 0x103
> +
> +#define MSG_RETRY 3
> +#define RETRY_SLEEP_MS 1
> +
> +static struct socfpga_fcs_priv *priv;
> +
> +/**
> + * fcs_atf_version_callback() - service-layer callback for the ATF version query
> + * @client: pointer to the stratix10-svc client
> + * @data: pointer to the service-layer callback data
> + *
> + * Store the returned Arm Trusted Firmware version (or mailbox error) in @priv
> + * and signal completion to the waiting caller.
> + */
> +static void fcs_atf_version_callback(struct stratix10_svc_client *client,
> + struct stratix10_svc_cb_data *data)
> +{
> + struct socfpga_fcs_priv *p = client->priv;
> +
> + p->status = data->status;
> + if (data->status == BIT(SVC_STATUS_OK)) {
> + p->status = 0;
> + p->atf_version[0] = *((unsigned int *)data->kaddr1);
> + p->atf_version[1] = *((unsigned int *)data->kaddr2);
> + p->atf_version[2] = *((unsigned int *)data->kaddr3);
> + } else if (data->status == BIT(SVC_STATUS_ERROR)) {
> + p->status = *((unsigned int *)data->kaddr1);
> + dev_err(client->dev, "mbox_error=0x%x\n", p->status);
> + }
> +
> + complete(&p->completion);
> +}
> +
> +/**
> + * fcs_async_callback() - completion callback for an async service request
> + * @ptr: pointer to the completion to signal
> + */
> +static void fcs_async_callback(void *ptr)
> +{
> + if (ptr)
> + complete(ptr);
> +}
> +
> +/**
> + * fcs_svc_send_request() - build and send an FCS command to the service layer
> + * @command: FCS command code to dispatch
> + * @timeout: time to wait for completion, in jiffies
> + *
> + * Build the service-layer message for @command and send it through the
> + * stratix10-svc service driver, using the synchronous path for the ATF version
> + * query and the asynchronous mailbox path (with retries) for the remaining
> + * commands.
> + *
> + * Return: 0 on success, negative errno on failure.
> + */
> +static int fcs_svc_send_request(enum fcs_command_code command,
> + unsigned long timeout)
> +{
> + struct fcs_cmd_context *k_ctx = &priv->k_ctx;
> + struct stratix10_svc_cb_data data;
> + void *handle = NULL;
> + int status, index;
> + int ret = 0;
> + struct stratix10_svc_client_msg *msg = kzalloc(sizeof(*msg), GFP_KERNEL);
> +
> + priv->status = 0;
> + priv->resp = 0;
> +
> + switch (command) {
> + case FCS_DEV_CRYPTO_OPEN_SESSION:
> + pr_debug("Sending command: COMMAND_FCS_CRYPTO_OPEN_SESSION\n");
> + msg->command = COMMAND_FCS_CRYPTO_OPEN_SESSION;
> + break;
> +
> + case FCS_DEV_CRYPTO_CLOSE_SESSION:
> + pr_debug("Sending command: COMMAND_FCS_CRYPTO_CLOSE_SESSION with session_id: 0x%x\n",
> + priv->session_id);
> + msg->arg[0] = priv->session_id;
> + msg->command = COMMAND_FCS_CRYPTO_CLOSE_SESSION;
> + break;
> +
> + case FCS_DEV_ATF_VERSION:
> + pr_debug("Sending command: COMMAND_SMC_ATF_BUILD_VER\n");
> + msg->command = COMMAND_SMC_ATF_BUILD_VER;
> + break;
> +
> + case FCS_DEV_SDOS_DATA_EXT:
> + pr_debug("Sending command: COMMAND_FCS_SDOS_DATA_EXT with session_id: 0x%x, context_id: 0x%x, op_mode: 0x%x, own: 0x%llx\n",
> + priv->session_id, k_ctx->sdos.context_id,
> + k_ctx->sdos.op_mode, k_ctx->sdos.own);
> + msg->arg[0] = priv->session_id;
> + msg->arg[1] = k_ctx->sdos.context_id;
> + msg->arg[2] = k_ctx->sdos.op_mode;
> + msg->arg[3] = k_ctx->sdos.own;
> + msg->payload = k_ctx->sdos.src;
> + msg->payload_length = k_ctx->sdos.src_size;
> + msg->payload_output = k_ctx->sdos.dst;
> + msg->payload_length_output = *k_ctx->sdos.dst_size;
> + msg->command = COMMAND_FCS_SDOS_DATA_EXT;
> + break;
> +
> + default:
> + pr_err("Unknown command: 0x%x\n", command);
> + ret = -EINVAL;
> + break;
> + }
> +
> + if (ret) {
> + kfree(msg);
> + return ret;
> + }
> +
> + if (command == FCS_DEV_ATF_VERSION) {
> + reinit_completion(&priv->completion);
> +
> + /*
> + * receive_cb is a persistent field on the shared client and
> + * is only consumed by the synchronous stratix10_svc_send()
> + * path. Set it immediately before the send and clear it right
> + * after so it is non-NULL only for the duration of this
> + * transaction. This keeps the callback correct per command and
> + * prevents a future synchronous caller from silently
> + * inheriting a stale fcs_atf_version_callback.
> + */
> + priv->client.receive_cb = fcs_atf_version_callback;
> +
> + ret = stratix10_svc_send(priv->chan, msg);
> + if (ret) {
> + pr_err("failed to send message to service channel\n");
> + goto fun_ret;
> + }
> +
> + if (!wait_for_completion_timeout(&priv->completion,
> + msecs_to_jiffies(timeout))) {
> + pr_err("svc timeout to get completed status\n");
> + ret = -ETIMEDOUT;
> + }
> +fun_ret:
> + priv->client.receive_cb = NULL;
> + kfree(msg);
> + return ret;
> + }
> Could this cause a null pointer dereference if the request times out?
> If wait_for_completion_timeout() expires, receive_cb is set to NULL and the
> message is freed, but the service layer's background thread continues
> processing the command. When the firmware eventually responds, the thread
> might attempt to unconditionally call the cleared receive_cb function.
when timeout happen, and priv->client.receive_cb set to NULL might create
uncertainty. will fix this by set NULL when sending this message fail.
will fix in v3.
> +
> + /*
> + * Use the device-lifetime priv->completion as the async callback arg
> + * rather than an on-stack completion: on a timeout/abort this function
> + * returns while the svc layer still holds a pointer to it in the
> + * transaction handle, so a stack object would be freed under it. FCS
> + * serializes commands under priv->lock (one in-flight), so reusing
> + * priv->completion here is safe.
> + */
> + reinit_completion(&priv->completion);
> +
> + for (index = 0; index < MSG_RETRY; index++) {
> + status = stratix10_svc_async_send(priv->chan, msg, &handle,
> + fcs_async_callback,
> + &priv->completion);
> + if (status == 0)
> + break;
> + msleep(RETRY_SLEEP_MS);
> + }
> +
> + if (!handle || status != 0) {
> + pr_err("Failed to send async message\n");
> + kfree(msg);
> + return -ETIMEDOUT;
> + }
> +
> + ret = wait_for_completion_io_timeout(&priv->completion,
> + msecs_to_jiffies(timeout));
> + if (ret > 0)
> + pr_debug("Received async interrupt\n");
> + else
> + pr_err("timeout occurred while waiting for async message\n");
> +
> Does the service layer actually signal this completion asynchronously?
> Looking at stratix10_svc_async_send(), it assigns the callback but the service
> layer doesn't seem to have a background mechanism to invoke it upon
> completion. It appears every asynchronous crypto request might needlessly
> block for the full 2-second timeout before successfully falling back to
> stratix10_svc_async_poll() below.
agree, this wait for timeout might be an extra blind wait for 2s.
will replace with a proper poll loop instead of always wait 2s.
will fix in v3.
> + ret = stratix10_svc_async_poll(priv->chan, handle, &data);
> +
> + if (ret == -EAGAIN) {
> + /*
> + * SDM still owns this transaction (STATUS_BUSY). Skip
> + * stratix10_svc_async_done() so its transaction_id is not
> + * recycled while in flight (which would alias responses); the
> + * handle/id are reclaimed at teardown and the caller can retry.
> + * msg is left unfreed to avoid a dangling handle->msg (small,
> + * bounded leak). Safe only while completions are poll-delivered.
> + */
> + pr_err("SDM transaction is busy, aborting\n");
> + return -EINPROGRESS;
> + }
> +
> Does returning early here without cleaning up the transaction handle lead to
> a use-after-free during module teardown?
> By intentionally skipping stratix10_svc_async_done(), the transaction handle
> remains linked in the service layer's actrl->trx_list hash table. Later, when
> the driver is unbound, fcs_deinit() calls stratix10_svc_remove_async_client()
> which frees the achan structure. If the stratix10-svc layer is unloaded, its
> cleanup routine could iterate over the stale handle and dereference the
> freed handler->achan pointer.
will fix in v3 where when error happen, will to to the state of asyn done and
unhashes the handler.
> + if (ret) {
> + pr_err("Failed to poll async message\n");
> + goto out;
> + }
> +
> + priv->status = data.status;
> +
> + if (data.kaddr1)
> + priv->resp = *((u32 *)data.kaddr1);
> + else
> + priv->resp = 0;
> +
> +out:
> + stratix10_svc_async_done(priv->chan, handle);
> + kfree(msg);
> +
> + return ret;
> +}
> +
> +/**
> + * fcs_session_open() - open an FCS crypto service session
> + * @k_ctx: pointer to the kernel-side FCS command context
> + *
> + * Request a new session from the SDM, generate the session UUID and copy it,
> + * together with the mailbox status, back to user space.
> + *
> + * Return: 0 on success, negative errno on failure.
> + */
> +int fcs_session_open(struct fcs_cmd_context *const k_ctx)
> +{
> + int ret = 0;
> +
> + ret = fcs_svc_send_request(FCS_DEV_CRYPTO_OPEN_SESSION,
> + SVC_FCS_REQUEST_TIMEOUT_MS);
> + if (ret) {
> + pr_err("Failed to send the cmd=%d,ret=%d\n",
> + FCS_DEV_CRYPTO_OPEN_SESSION, ret);
> + return ret;
> + }
> +
> + if (priv->status) {
> + ret = -EIO;
> + pr_err("Mailbox error, Failed to open session ret: %d\n", ret);
> + goto copy_mbox_status;
> + }
> +
> + uuid_gen(&priv->uuid_id);
> +
> + memcpy(&priv->session_id, &priv->resp, sizeof(priv->session_id));
> +
> Can a user space program exhaust firmware sessions by repeatedly writing to
> this interface?
> It looks like priv->session_id is overwritten without first checking if a
> session is already open or requesting the SDM to close it. Repeated calls to
> fcs_session_open() might leave orphaned sessions in the firmware, eventually
> leading to a denial of service for cryptographic operations.
The SDM only allows a single crypto-service session at a time and rejects a
new session id while another one is open. Therefore no orphaned sessios
cannot accumulate. But there's no harm to guard on driver side,
will add in v3.
> + ret = copy_to_user(k_ctx->open_session.suuid, &priv->uuid_id,
> + sizeof(uuid_t)) ? -EFAULT : 0;
> + if (ret) {
> + pr_err("Failed to copy session ID to user suuid addr: %p ret: %d\n",
> + k_ctx->open_session.suuid, ret);
> + }
> +
> +copy_mbox_status:
> + if (copy_to_user(k_ctx->error_code_addr, &priv->status,
> + sizeof(priv->status))) {
> + pr_err("Failed to copy mail box status code to user\n");
> + /* surface the copy failure only if nothing failed earlier */
> + if (!ret)
> + ret = -EFAULT;
> + }
> +
> + return ret;
> +}
> +
> +/**
> + * fcs_session_close() - close an FCS crypto service session
> + * @k_ctx: pointer to the kernel-side FCS command context
> + *
> + * Validate the caller-supplied session UUID, ask the SDM to close the session
> + * and copy the mailbox status back to user space.
> + *
> + * Return: 0 on success, negative errno on failure.
> + */
> +int fcs_session_close(struct fcs_cmd_context *const k_ctx)
> +{
> + int ret = 0;
> + struct fcs_cmd_context ctx;
> +
> + memcpy(&ctx, k_ctx, sizeof(struct fcs_cmd_context));
> +
> + if (!uuid_equal(&priv->uuid_id, &ctx.close_session.suuid)) {
> + ret = -EINVAL;
> + pr_err("Session UUID Mismatch ret: %d\n", ret);
> + return ret;
> + }
> +
> + ret = fcs_svc_send_request(FCS_DEV_CRYPTO_CLOSE_SESSION,
> + SVC_FCS_REQUEST_TIMEOUT_MS);
> + if (ret) {
> + pr_err("Failed to send the cmd=%d,ret=%d\n",
> + FCS_DEV_CRYPTO_CLOSE_SESSION, ret);
> + return ret;
> + }
> +
> + memset(&priv->uuid_id, 0, sizeof(uuid_t));
> + priv->session_id = 0;
> + if (priv->status) {
> + ret = -EIO;
> + pr_err("Mailbox error, Failed to close session ret: %d\n", ret);
> + }
> +
> + if (copy_to_user(ctx.error_code_addr, &priv->status,
> + sizeof(priv->status))) {
> + pr_err("Failed to copy mail box status code to user\n");
> + /* surface the copy failure only if nothing failed earlier */
> + if (!ret)
> + ret = -EFAULT;
> + }
> +
> + return ret;
> +}
> +
> +/**
> + * fcs_get_atf_version() - return the cached Arm Trusted Firmware version
> + * @version: array of three u32 entries to receive the major, minor and patch
> + * version numbers
> + */
> +void fcs_get_atf_version(u32 *version)
> +{
> + memcpy(version, priv->atf_version, sizeof(priv->atf_version));
> +}
> +
> +/**
> + * fcs_sdos_crypt() - perform an SDOS encrypt or decrypt operation
> + * @k_ctx: pointer to the kernel-side FCS command context
> + *
> + * Allocate service-layer source and destination buffers, copy the input from
> + * user space, drive the SDOS data command and copy the result and length back
> + * to user space. The operation direction is selected by @k_ctx->sdos.op_mode.
> + *
> + * Return: 0 on success, negative errno on failure.
> + */
> +int fcs_sdos_crypt(struct fcs_cmd_context *const k_ctx)
> +{
> + void *s_buf = NULL, *d_buf = NULL;
> + struct fcs_cmd_context ctx;
> + u32 output_size;
> + u32 dst_cap;
> + u64 owner_id;
> + int ret = 0;
> +
> + memcpy(&ctx, k_ctx, sizeof(struct fcs_cmd_context));
> +
> + /* Authorize the caller against the open session before doing any work */
> + if (!uuid_equal(&priv->uuid_id, &ctx.sdos.suuid)) {
> + pr_err("Session UUID mismatch\n");
> + return -EINVAL;
> + }
> +
> + if (!ctx.sdos.dst || !ctx.sdos.dst_size)
> + return -EINVAL;
> +
> + /* Caller-provided output buffer capacity (in/out parameter) */
> + if (copy_from_user(&dst_cap, ctx.sdos.dst_size, sizeof(dst_cap)))
> + return -EFAULT;
> +
> + if (ctx.sdos.op_mode) {
> + output_size = SDOS_ENCRYPTED_MAX_SZ;
> + /* encrypt: input is header + plaintext */
> + if (ctx.sdos.src_size < SDOS_DECRYPTED_MIN_SZ ||
> + ctx.sdos.src_size > SDOS_DECRYPTED_MAX_SZ) {
> + pr_err("Invalid SDOS src_size %u\n", ctx.sdos.src_size);
> + return -EINVAL;
> + }
> + } else {
> + output_size = SDOS_DECRYPTED_MAX_SZ;
> + /* decrypt: input is header + plaintext + HMAC */
> + if (ctx.sdos.src_size < SDOS_ENCRYPTED_MIN_SZ ||
> + ctx.sdos.src_size > SDOS_ENCRYPTED_MAX_SZ) {
> + pr_err("Invalid SDOS src_size %u\n", ctx.sdos.src_size);
> + return -EINVAL;
> + }
> + }
> +
> + s_buf = stratix10_svc_allocate_memory(priv->chan, ctx.sdos.src_size);
> + if (IS_ERR(s_buf)) {
> + ret = -ENOMEM;
> + pr_err("Failed to allocate memory for SDOS input data kernel buffer ret: %d\n",
> + ret);
> + return ret;
> + }
> +
> + /*
> + * The remaining k_ctx writes intentionally target priv->k_ctx (k_ctx
> + * points at it): fcs_svc_send_request() reads the outgoing request from
> + * priv->k_ctx, so the kernel buffers and params are staged there rather
> + * than in the local ctx snapshot. dst_size must point at device-lifetime
> + * storage (priv->sdos_output_size), never a caller stack variable, so
> + * the staged pointer cannot dangle after this function returns.
> + */
> + priv->sdos_output_size = output_size;
> + k_ctx->sdos.dst_size = &priv->sdos_output_size;
> +
> + d_buf = stratix10_svc_allocate_memory(priv->chan, output_size);
> + if (IS_ERR(d_buf)) {
> + ret = -ENOMEM;
> + pr_err("Failed to allocate memory for SDOS output kernel buffer ret: %d\n", ret);
> + goto free_sbuf;
> + }
> +
> + /* Copy the user space input data to the input data kernel buffer */
> + ret = copy_from_user(s_buf, ctx.sdos.src,
> + ctx.sdos.src_size) ? -EFAULT : 0;
> + if (ret) {
> + pr_err("Failed to copy SDOS data from user to kernel buffer ret: %d\n", ret);
> + goto free_dbuf;
> + }
> +
> + /* Get Owner ID from buf */
> + memcpy(&owner_id, (u8 *)s_buf + OWNER_ID_OFFSET, OWNER_ID_SIZE);
> + k_ctx->sdos.own = owner_id;
> Will this properly handle owner_id endianness on big-endian hosts?
> Using memcpy() to copy 8 bytes from the raw data stream directly into a u64
> will result in host-dependent byte ordering. If the parsed integer is later
> sent to the SDM via a register, a big-endian system will reverse the bytes
> compared to a little-endian system.
agree, the memcpy() into a u64 interprets the 8 header bytes in host byte order,
so the value placed in the SMC register (msg->arg[3]) would differ on a
big-endian host. The owner_id is stored little-endian in the SDOS header,
so I've switched to an explicit, alignment-safe accessor using get_unaligned_le64
in v3.
> + k_ctx->sdos.src = s_buf;
> + k_ctx->sdos.dst = d_buf;
> +
> + ret = fcs_svc_send_request(FCS_DEV_SDOS_DATA_EXT,
> + SVC_FCS_REQUEST_TIMEOUT_MS);
> +
> Is a memory barrier required here before notifying the hardware?
> The memory for s_buf is allocated from the service layer's genpool which is
> mapped with MEMREMAP_WC (Write-Combined). Since the SMC instruction on ARM64
> acts as a context synchronization event but does not flush store buffers, the
> CPU might still be holding the plaintext writes when the SDM begins its DMA,
> potentially resulting in data corruption.
Agreed that a barrier is needed. However, the affected memory is allocated and
mapped MEMREMAP_WC by the stratix10-svc service layer, and the SMC is issued
there via invoke_fn() — this path is shared by all svc clients (RSU, FPGA mgr,
etc.), and the missing barrier pre-dates this series.
adding the barrier before invoke_fn() in stratix10-svc.c
(with an appropriate Fixes: tag) so it's reviewed on its own and can be
backported independently. This FCS series doesn't introduce the issue.
> + if (ret == -EINPROGRESS) {
> + /*
> + * SDM still owns this transaction and may still DMA into
> + * d_buf. Do NOT free s_buf/d_buf or copy results: returning
> + * them to the gen_pool would let the delayed firmware write
> + * corrupt reallocated memory. Leak them along with the
> + * abandoned transaction (bounded, exceptional stuck-SDM path).
> + */
> + return -ETIMEDOUT;
> + }
> +
> + if (ret) {
> + pr_err("Failed to send the cmd=%d,ret=%d\n", FCS_DEV_SDOS_DATA_EXT, ret);
> + goto free_dbuf;
> + }
> + if (priv->status &&
> + priv->status != SDOS_DECRYPTION_REPROVISION_KEY_WARN &&
> + priv->status != SDOS_DECRYPTION_NOT_LATEST_KEY_WARN) {
> + pr_err("Failed to perform SDOS operation ret: %d Mailbox Status = %d\n",
> + ret, priv->status);
> + goto copy_mbox_status;
> + }
> +
> + /*
> + * priv->resp is reported by firmware; never trust it to read back
> + * more than the kernel output buffer (d_buf) actually holds,
> + * otherwise the copy below would leak adjacent kernel memory.
> + */
> + if (priv->resp > output_size) {
> + pr_err("SDOS output %u exceeds kernel buffer %u\n",
> + priv->resp, output_size);
> + ret = -EIO;
> + goto copy_mbox_status;
> + }
> +
> + /* Do not write past the caller-provided output buffer */
> + if (priv->resp > dst_cap) {
> + pr_err("SDOS output %u exceeds caller buffer %u\n",
> + priv->resp, dst_cap);
> + ret = -EMSGSIZE;
> + goto copy_mbox_status;
> + }
> +
> + /* Copy the encrypted/decrypted output from kernel space to user space */
> + ret = copy_to_user(ctx.sdos.dst, d_buf, priv->resp) ? -EFAULT : 0;
> + if (ret) {
> + pr_err("Failed to copy encrypted output to user ret: %d\n", ret);
> + goto copy_mbox_status;
> + }
> +
> + /* Copy the encrypted output length from kernel space to user space */
> + ret = copy_to_user(ctx.sdos.dst_size, &priv->resp,
> + sizeof(priv->resp)) ? -EFAULT : 0;
> + if (ret)
> + pr_err("Failed to copy encrypted output length to user ret: %d\n", ret);
> +
> +copy_mbox_status:
> + if (copy_to_user(ctx.error_code_addr, &priv->status,
> + sizeof(priv->status))) {
> + pr_err("Failed to copy mailbox status code to user\n");
> + /* surface the copy failure only if nothing failed earlier */
> + if (!ret)
> + ret = -EFAULT;
> + }
> +free_dbuf:
> + stratix10_svc_free_memory(priv->chan, d_buf);
> +free_sbuf:
> + stratix10_svc_free_memory(priv->chan, s_buf);
> +
> + return ret;
> +}
> +
> +/**
> + * fcs_acquire_cmd_ctx() - take the FCS lock and return the command context
> + *
> + * Serialises access to the shared command context across concurrent callers.
> + * The caller must release it with fcs_release_cmd_ctx().
> + *
> + * Return: pointer to the locked FCS command context.
> + */
> +struct fcs_cmd_context *fcs_acquire_cmd_ctx(void)
> +{
> + if (!priv)
> + return NULL;
> +
> + mutex_lock(&priv->lock);
> + return &priv->k_ctx;
> +}
> +
> +/**
> + * fcs_release_cmd_ctx() - release the FCS command context lock
> + * @k_ctx: pointer to the FCS command context previously acquired
> + */
> +void fcs_release_cmd_ctx(struct fcs_cmd_context *const k_ctx)
> +{
> + mutex_unlock(&priv->lock);
> +}
> +
> +/**
> + * fcs_read_version_from_atf() - query the Arm Trusted Firmware build version
> + *
> + * Send the ATF version command to the SDM and cache the result in @priv.
> + *
> + * Return: 0 on success, negative errno on failure.
> + */
> +static int fcs_read_version_from_atf(void)
> +{
> + int ret = 0;
> +
> + ret = fcs_svc_send_request(FCS_DEV_ATF_VERSION,
> + SVC_FCS_REQUEST_TIMEOUT_MS);
> + if (ret) {
> + pr_err("Failed to send the cmd=%d,ret=%d\n", FCS_DEV_ATF_VERSION, ret);
> + return ret;
> + }
> +
> + if (priv->status) {
> + ret = -EIO;
> + pr_err("Mailbox error, Failed to read ATF version ret: %d\n", ret);
> + }
> +
> + stratix10_svc_done(priv->chan);
> +
> + return ret;
> +}
> +
> +/**
> + * fcs_init() - allocate and initialise the FCS private state
> + * @dev: pointer to fcs device
> + *
> + * Allocate @priv, request the service channel, register the async client,
> + * and read the ATF version.
> + *
> + * Return: 0 on success, -EPROBE_DEFER or negative errno on failure.
> + */
> +int fcs_init(struct device *dev)
> +{
> + int ret;
> +
> + if (priv)
> + return -EBUSY; /* singleton: one FCS instance only */
> +
> + priv = devm_kzalloc(dev, sizeof(struct socfpga_fcs_priv), GFP_KERNEL);
> + if (!priv)
> + return -ENOMEM;
> +
> + mutex_init(&priv->lock);
> +
> + priv->dev = dev;
> + priv->client.dev = dev;
> + priv->client.receive_cb = NULL;
> + priv->client.priv = priv;
> +
> + priv->chan = stratix10_svc_request_channel_byname(&priv->client,
> + SVC_CLIENT_FCS);
> + if (IS_ERR(priv->chan)) {
> + pr_err("couldn't get service channel %s\n", SVC_CLIENT_FCS);
> + return -EPROBE_DEFER;
> + }
> +
> Does returning an error here leave a dangling pointer in the global priv?
> Since priv is allocated using devm_kzalloc(), the driver core will free the
> memory upon a probe failure like -EPROBE_DEFER. However, the global priv
> variable isn't set back to NULL. On a subsequent probe attempt, the initial
> check (if (priv)) will see a non-NULL dangling pointer and return -EBUSY,
> permanently preventing the driver from initializing.
okay, make sense. Will reset the global priv to NULL if devm_kzalloc failed.
will fix in v3
> + ret = stratix10_svc_add_async_client(priv->chan, true);
> + if (ret) {
> + pr_err("Failed to add async client\n");
> + goto free_chan;
> + }
> +
> + init_completion(&priv->completion);
> +
> + fcs_read_version_from_atf();
> +
> + return 0;
> +
> +free_chan:
> + stratix10_svc_free_channel(priv->chan);
> +
> + return ret;
> +}
> +
> +/**
> + * fcs_deinit() - tear down the FCS private state
> + *
> + * Close any open session, remove the async client, free the service channel
> + * and clear @priv.
> + */
> +void fcs_deinit(void)
> +{
> + if (priv && priv->session_id) {
> + int ret = fcs_svc_send_request(FCS_DEV_CRYPTO_CLOSE_SESSION,
> + SVC_FCS_REQUEST_TIMEOUT_MS);
> + if (ret)
> + pr_err("Failed to close FCS service session,ret=%d\n", ret);
> + }
> +
> + if (priv) {
> + stratix10_svc_remove_async_client(priv->chan);
> + stratix10_svc_free_channel(priv->chan);
> + }
> +
> + priv = NULL;
> +}
> +
> +/**
> + * fcs_cleanup() - release the FCS service channel and clear the state
> + */
> +void fcs_cleanup(void)
> +{
> + if (priv)
> + stratix10_svc_free_channel(priv->chan);
> +
> + priv = NULL;
> +}
> diff --git a/drivers/firmware/socfpga-fcs.c b/drivers/firmware/socfpga-fcs.c
> new file mode 100644
> index 000000000000..616f2735743a
> --- /dev/null
> +++ b/drivers/firmware/socfpga-fcs.c
> @@ -0,0 +1,250 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/*
> + * Copyright (C) 2026, Altera Corporation
> + */
> +
> +#include <linux/firmware/intel/socfpga-fcs.h>
> +#include <linux/of.h>
> +#include <linux/of_platform.h>
> +#include <linux/platform_device.h>
> +#include <linux/sysfs.h>
> +
> +/**
> + * open_session_store() - open an FCS crypto service session
> + * @dev: pointer to fcs device
> + * @attr: device attribute
> + * @buf: pointer to character buffer carrying the user command context
> + * @buf_size: size of the buffer
> + *
> + * Return: @buf_size on success, negative errno on failure.
> + */
> +static ssize_t open_session_store(struct device *dev,
> + struct device_attribute *attr,
> + const char *buf, size_t buf_size)
> +{
> + struct fcs_cmd_context *const u_ctx = *(struct fcs_cmd_context **)buf;
> Is it safe to blindly cast and dereference the sysfs string buffer as a pointer?
> The buf parameter provided by the sysfs store operation comes from userspace
> text input. kernfs_fop_write_iter() allocates a buffer of user-provided count + 1
> bytes. If a user writes fewer than 8 bytes, this pointer dereference will read
> past the allocated heap memory, triggering a KASAN out-of-bounds read. In
> addition, expecting raw binary pointers over sysfs breaks compatibility on
> 32-bit user space systems.
> This pattern appears to be repeated in close_session_store() and sdos_store()
> as well.
since this is violate sysfs and the compatibility issue, will use ioctl to store
the data from userspace in v3.
> + struct fcs_cmd_context *k_ctx;
> + int ret;
> +
> + k_ctx = fcs_acquire_cmd_ctx();
> + if (!k_ctx) {
> + dev_err(dev, "Failed get context. Context is in use\n");
> + return -EBUSY;
> + }
> +
> + if (copy_from_user(k_ctx, u_ctx, sizeof(*k_ctx))) {
> + dev_err(dev, "Failed to copy context from user space\n");
> + ret = -EFAULT;
> + goto out;
> + }
> +
> + ret = fcs_session_open(k_ctx);
> + if (ret) {
> + dev_err(dev, "Failed to open session\n");
> + goto out;
> + }
> +
> + ret = buf_size;
> +out:
> + fcs_release_cmd_ctx(k_ctx);
> +
> + return ret;
> +}
> +
> +/**
> + * close_session_store() - close an FCS crypto service session
> + * @dev: pointer to fcs device
> + * @attr: device attribute
> + * @buf: pointer to character buffer carrying the user command context
> + * @buf_size: size of the buffer
> + *
> + * Return: @buf_size on success, negative errno on failure.
> + */
> +static ssize_t close_session_store(struct device *dev,
> + struct device_attribute *attr,
> + const char *buf, size_t buf_size)
> +{
> + struct fcs_cmd_context *const u_ctx = *(struct fcs_cmd_context **)buf;
> + struct fcs_cmd_context *k_ctx;
> + int ret;
> +
> + k_ctx = fcs_acquire_cmd_ctx();
> + if (!k_ctx) {
> + dev_err(dev, "Failed get context. Context is in use\n");
> + return -EBUSY;
> + }
> +
> + if (copy_from_user(k_ctx, u_ctx, sizeof(*k_ctx))) {
> + dev_err(dev, "Failed to copy context from user space\n");
> + ret = -EFAULT;
> + goto out;
> + }
> +
> + ret = fcs_session_close(k_ctx);
> + if (ret) {
> + dev_err(dev, "Failed to close session\n");
> + goto out;
> + }
> +
> + ret = buf_size;
> +out:
> + fcs_release_cmd_ctx(k_ctx);
> +
> + return ret;
> +}
> +
> +/**
> + * atf_version_show() - report the Arm Trusted Firmware build version
> + * @dev: pointer to fcs device
> + * @attr: device attribute
> + * @buf: pointer to character buffer to receive the version string
> + *
> + * Return: number of bytes written to @buf.
> + */
> +static ssize_t atf_version_show(struct device *dev,
> + struct device_attribute *attr, char *buf)
> +{
> + int version[3];
> +
> + fcs_get_atf_version(version);
> + return sysfs_emit(buf, "%u.%u.%u\n", version[0], version[1], version[2]);
> +}
> +
> +/**
> + * sdos_store() - perform an SDOS encrypt/decrypt operation
> + * @dev: pointer to fcs device
> + * @attr: device attribute
> + * @buf: pointer to character buffer carrying the user command context
> + * @buf_size: size of the buffer
> + *
> + * Return: @buf_size on success, negative errno on failure.
> + */
> +static ssize_t sdos_store(struct device *dev, struct device_attribute *attr,
> + const char *buf, size_t buf_size)
> +{
> + struct fcs_cmd_context *const u_ctx = *(struct fcs_cmd_context **)buf;
> + struct fcs_cmd_context *k_ctx;
> + int ret;
> +
> + k_ctx = fcs_acquire_cmd_ctx();
> + if (!k_ctx) {
> + dev_err(dev, "Failed get context. Context is in use\n");
> + return -EBUSY;
> + }
> +
> + if (copy_from_user(k_ctx, u_ctx, sizeof(*k_ctx))) {
> + dev_err(dev, "Failed to copy context from user space\n");
> + ret = -EFAULT;
> + goto out;
> + }
> +
> + ret = fcs_sdos_crypt(k_ctx);
> + if (ret) {
> + dev_err(dev, "Failed to perform SDOS operation\n");
> + goto out;
> + }
> +
> + ret = buf_size;
> +out:
> + fcs_release_cmd_ctx(k_ctx);
> +
> + return ret;
> +}
> +
> +static DEVICE_ATTR_WO(open_session);
> +static DEVICE_ATTR_WO(close_session);
> +static DEVICE_ATTR_RO(atf_version);
> +static DEVICE_ATTR_WO(sdos);
> +
> +static struct attribute *fcs_attrs[] = {
> + &dev_attr_open_session.attr,
> + &dev_attr_close_session.attr,
> + &dev_attr_atf_version.attr,
> + &dev_attr_sdos.attr,
> + NULL
> +};
> +
> +static struct attribute_group fcs_group = {
> + .attrs = fcs_attrs,
> +};
> +
> +static const struct attribute_group *fcs_groups[] = {
> + &fcs_group,
> + NULL,
> +};
> +
> +/**
> + * fcs_driver_probe() - probe the FCS platform device
> + * @pdev: pointer to the FCS platform device
> + *
> + * Initialise the FCS state. The sysfs attribute groups are published
> + * automatically by the driver core via fcs_driver.driver.dev_groups.
> + *
> + * Return: 0 on success, negative errno on failure.
> + */
> +static int fcs_driver_probe(struct platform_device *pdev)
> +{
> + struct device *dev = &pdev->dev;
> + int ret;
> +
> + ret = fcs_init(dev);
> + if (ret) {
> + dev_err(dev, "Failed to initialize FCS\n");
> + return ret;
> + }
> +
> + return 0;
> +}
> +
> +/**
> + * fcs_driver_remove() - remove the FCS platform device
> + * @pdev: pointer to the FCS platform device
> + *
> + * Tear down the FCS state. The sysfs attribute groups are removed
> + * automatically by the driver core.
> + */
> +static void fcs_driver_remove(struct platform_device *pdev)
> +{
> + fcs_deinit();
> +}
> +
> +static struct platform_driver fcs_driver = {
> + .probe = fcs_driver_probe,
> + .remove = fcs_driver_remove,
> + .driver = {
> + .name = "stratix10-fcs",
> + .dev_groups = fcs_groups,
> + },
> +};
> +
> +/**
> + * socfpga_fcs_init() - register the FCS platform driver
> + *
> + * Return: 0 on success, negative errno on failure.
> + */
> +static int __init socfpga_fcs_init(void)
> +{
> + int ret;
> +
> + ret = platform_driver_register(&fcs_driver);
> + if (ret)
> + pr_err("Failed to register platform driver: %d\n", ret);
> +
> + return ret;
> +}
> +
> +/**
> + * socfpga_fcs_exit() - unregister the FCS platform driver
> + */
> +static void __exit socfpga_fcs_exit(void)
> +{
> + platform_driver_unregister(&fcs_driver);
> +}
> +
> +module_init(socfpga_fcs_init);
> +module_exit(socfpga_fcs_exit);
> +
> +MODULE_LICENSE("GPL");
> +MODULE_DESCRIPTION("Altera SoCFPGA FCS SDOS encrypt/decrypt driver");
> +MODULE_AUTHOR("Altera Corporation");
> +MODULE_ALIAS("platform:stratix10-fcs");
> diff --git a/include/linux/firmware/intel/socfpga-fcs.h b/include/linux/firmware/intel/socfpga-fcs.h
> new file mode 100644
> index 000000000000..3203be4f0f90
> --- /dev/null
> +++ b/include/linux/firmware/intel/socfpga-fcs.h
> @@ -0,0 +1,133 @@
> +/* SPDX-License-Identifier: GPL-2.0-only */
> +/*
> + * Copyright (C) 2026 Altera Corporation
> + *
> + * SDOS-only subset of the SoCFPGA FCS (FPGA Crypto Service) interface,
> + * shared between the driver front-end (socfpga-fcs.c) and the command
> + * engine (socfpga-fcs-core.c).
> + */
> +#ifndef SOCFPGA_FCS_H
> +#define SOCFPGA_FCS_H
> +
> +#include <linux/completion.h>
> +#include <linux/device.h>
> +#include <linux/mutex.h>
> +#include <linux/types.h>
> +#include <linux/uuid.h>
> +#include <linux/firmware/intel/stratix10-svc-client.h>
> +
> +#define SDOS_HEADER_SZ 40
> +#define SDOS_HMAC_SZ 48
> +#define SDOS_PLAINDATA_MIN_SZ 32
> +#define SDOS_PLAINDATA_MAX_SZ 32672
> +#define SDOS_DECRYPTED_MIN_SZ (SDOS_PLAINDATA_MIN_SZ + SDOS_HEADER_SZ)
> +#define SDOS_DECRYPTED_MAX_SZ (SDOS_PLAINDATA_MAX_SZ + SDOS_HEADER_SZ)
> +#define SDOS_ENCRYPTED_MIN_SZ (SDOS_PLAINDATA_MIN_SZ + SDOS_HEADER_SZ + SDOS_HMAC_SZ)
> +#define SDOS_ENCRYPTED_MAX_SZ (SDOS_PLAINDATA_MAX_SZ + SDOS_HEADER_SZ + SDOS_HMAC_SZ)
> +
> +#pragma pack(push, 1)
> +struct fcs_cmd_context {
> + /* Error status variable address */
> + int *error_code_addr;
> + union {
> + struct {
> + uuid_t *suuid;
> + unsigned int *suuid_len;
> + } open_session;
> +
> + struct {
> + uuid_t suuid;
> + } close_session;
> +
> + struct {
> + uuid_t suuid;
> + u32 context_id;
> + char *rng;
> + u32 rng_len;
> + } rng;
> +
> + struct {
> + uuid_t suuid;
> + u32 context_id;
> + u32 op_mode;
> + char *src;
> + u32 src_size;
> + char *dst;
> + u32 *dst_size;
> + u16 id;
> + u64 own;
> + int pad;
> + } sdos;
> + };
> +};
> +
> +#pragma pack(pop)
> +
> +/**
> + * Private driver state for the SoCFPGA FCS that holds the SDM/ATF service
> + * channel, the shared command context and the lock that guards it, and the
> + * latest mailbox status/response.
> + */
> +struct socfpga_fcs_priv {
> + /* Communication channel */
> + struct stratix10_svc_chan *chan;
> + struct fcs_cmd_context k_ctx;
> + struct stratix10_svc_client client;
> + struct completion completion;
> + /*
> + * Serializes FCS command submission: guards the shared k_ctx and the
> + * single in-flight mailbox transaction (completion/status/resp) so only
> + * one SDM request is outstanding at a time. This is the lock taken by
> + * fcs_acquire_cmd_ctx() and dropped by fcs_release_cmd_ctx().
> + */
> + struct mutex lock;
> + int status;
> + u32 resp;
> + u32 session_id;
> + uuid_t uuid_id;
> + struct device *dev;
> + u32 atf_version[3];
> + /*
> + * Backing store for the SDOS output-buffer capacity. The outgoing
> + * request stages k_ctx.sdos.dst_size to point here (device-lifetime)
> + * instead of at a caller stack variable, so the pointer never dangles.
> + */
> + u32 sdos_output_size;
> +};
> +
> +enum fcs_command_code {
> + FCS_DEV_COMMAND_NONE = 0,
> + FCS_DEV_CRYPTO_OPEN_SESSION,
> + FCS_DEV_CRYPTO_CLOSE_SESSION,
> + FCS_DEV_SDOS_DATA_EXT,
> + FCS_DEV_ATF_VERSION,
> +};
> +
> +/* Take the FCS lock and return the shared command context. */
> +struct fcs_cmd_context *fcs_acquire_cmd_ctx(void);
> +
> +/* Release the FCS lock previously taken by fcs_acquire_cmd_ctx(). */
> +void fcs_release_cmd_ctx(struct fcs_cmd_context *const k_ctx);
> +
> +/* Allocate the FCS state and set up the service channel; read ATF version. */
> +int fcs_init(struct device *dev);
> +
> +/* Close any open session and release the service channel. */
> +void fcs_deinit(void);
> +
> +/* Release the service channel and clear the FCS state. */
> +void fcs_cleanup(void);
> +
> +/* Request the SDM to open a crypto service session. */
> +int fcs_session_open(struct fcs_cmd_context *const k_ctx);
> +
> +/* Request the SDM to close a previously opened session. */
> +int fcs_session_close(struct fcs_cmd_context *const k_ctx);
> +
> +/* Return the cached Arm Trusted Firmware build version. */
> +void fcs_get_atf_version(u32 *version);
> +
> +/* Perform an SDOS (Secure Data Object Service) encrypt/decrypt operation. */
> +int fcs_sdos_crypt(struct fcs_cmd_context *const k_ctx);
> +
> +#endif /* SOCFPGA_FCS_H */