git: ae31f2928596 - main - ufshci: add a passthrough ioctl
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
Date: Tue, 15 Sep 2026 01:47:48 UTC
The branch main has been updated by jaeyoon:
URL: https://cgit.FreeBSD.org/src/commit/?id=ae31f2928596f8c6efc8f136559562d864968dcc
commit ae31f2928596f8c6efc8f136559562d864968dcc
Author: Jaeyoon Choi <jaeyoon@FreeBSD.org>
AuthorDate: 2026-09-15 01:40:33 +0000
Commit: Jaeyoon Choi <jaeyoon@FreeBSD.org>
CommitDate: 2026-09-15 01:44:33 +0000
ufshci: add a passthrough ioctl
This ioctl is for a port of ufs-utils:
https://github.com/SanDisk-Open-Source/ufs-utils
The driver only exposed a CAM SIM. Reading a descriptor, an attribute
or a flag needs a query request, and a UniPro attribute needs a DME
command. The driver built both only for its own setup, so userland
could reach neither.
Add two ioctls on the control node. UFSHCI_PASSTHROUGH_CMD sends a
UPIU the caller built, sizes the request from its transaction code,
and copies the response UPIU back. UFSHCI_PASSTHROUGH_UIC carries the
four attribute commands and refuses the rest, which can drop the link
or power the device off. It keeps the raw argument2 so the caller can
read the result code the device reported, not just a failure.
Validate the input and bound it by what the controller can map. The
descriptor has no request length, so the controller reads it from the
UPIU header, and a header that declares more than was copied in would
reach past the descriptor. The ioctl layer copies output back only on
a zero return. So a command that reached the device is a success even
when it was refused. The caller reads the reason from the response
header, and an answerless failure comes back as EIO. Clear the
response before use so no stale bytes read as a device answer.
Reviewed by: imp (mentor)
Sponsored by: Samsung Electronics
Differential Revision: https://reviews.freebsd.org/D59559
---
sys/dev/ufshci/ufshci.h | 10 +-
sys/dev/ufshci/ufshci_ioctl.c | 236 ++++++++++++++++++++++++++++++++++++++++
sys/dev/ufshci/ufshci_ioctl.h | 14 ++-
sys/dev/ufshci/ufshci_private.h | 2 +
sys/dev/ufshci/ufshci_uic_cmd.c | 5 +-
5 files changed, 259 insertions(+), 8 deletions(-)
diff --git a/sys/dev/ufshci/ufshci.h b/sys/dev/ufshci/ufshci.h
index aaf98cc70d37..3a225d92cb60 100644
--- a/sys/dev/ufshci/ufshci.h
+++ b/sys/dev/ufshci/ufshci.h
@@ -366,6 +366,9 @@ _Static_assert(sizeof(struct ufshci_upiu_header) == 12,
#define UFSHCI_MAX_UPIU_SIZE 512
#define UFSHCI_UPIU_ALIGNMENT 8 /* UPIU requires 64-bit alignment. */
+/* UFS Spec 4.1, section 10.6.1: Total EHS Length counts 32 byte units. */
+#define UFSHCI_EHS_UNIT_SIZE 32
+
struct ufshci_upiu {
/* dword 0-2 */
struct ufshci_upiu_header header;
@@ -517,6 +520,9 @@ struct ufshci_query_param {
size_t desc_size;
};
+/* The data segment of a query UPIU, where a descriptor is carried. */
+#define UFSHCI_QUERY_DATA_SEGMENT_SIZE 256
+
struct ufshci_query_request_upiu {
/* dword 0-2 */
struct ufshci_upiu_header header;
@@ -543,7 +549,7 @@ struct ufshci_query_request_upiu {
/* dword 7 */
uint32_t reserved3;
- uint8_t command_data[256];
+ uint8_t command_data[UFSHCI_QUERY_DATA_SEGMENT_SIZE];
} __packed __aligned(4);
_Static_assert(sizeof(struct ufshci_query_request_upiu) == 288,
@@ -603,7 +609,7 @@ struct ufshci_query_response_upiu {
/* dword 7 */
uint8_t reserved4[4];
- uint8_t command_data[256];
+ uint8_t command_data[UFSHCI_QUERY_DATA_SEGMENT_SIZE];
} __packed __aligned(4);
_Static_assert(sizeof(struct ufshci_query_response_upiu) == 288,
diff --git a/sys/dev/ufshci/ufshci_ioctl.c b/sys/dev/ufshci/ufshci_ioctl.c
index 976b28de4cf5..ead404baea8e 100644
--- a/sys/dev/ufshci/ufshci_ioctl.c
+++ b/sys/dev/ufshci/ufshci_ioctl.c
@@ -8,9 +8,12 @@
#include <sys/param.h>
#include <sys/conf.h>
#include <sys/ioccom.h>
+#include <sys/malloc.h>
+#include <sys/systm.h>
#include "ufshci_private.h"
#include "ufshci_ioctl.h"
+#include "ufshci_reg.h"
static d_ioctl_t ufshci_ioctl;
@@ -20,11 +23,244 @@ static struct cdevsw ufshci_cdevsw = {
.d_name = "ufshci",
};
+/*
+ * Work out how much of the UPIU the controller has to read and how much
+ * it may write back.
+ */
+static int
+ufshci_passthrough_upiu_sizes(const struct ufshci_pt_command *pt,
+ size_t *req_size, size_t *resp_size, bool *is_admin)
+{
+ const struct ufshci_upiu_header *header = &pt->req_upiu.header;
+ size_t ehs_bytes = header->ehs_length * UFSHCI_EHS_UNIT_SIZE;
+ size_t max_data_len;
+
+ switch (header->trans_code) {
+ case UFSHCI_UPIU_TRANSACTION_CODE_QUERY_REQUEST:
+ /* Only a command UPIU has room for an EHS. */
+ if (header->ehs_length != 0)
+ return (EINVAL);
+ *req_size = sizeof(struct ufshci_query_request_upiu);
+ *resp_size = sizeof(struct ufshci_query_response_upiu);
+ *is_admin = true;
+ max_data_len = UFSHCI_QUERY_DATA_SEGMENT_SIZE;
+ break;
+ case UFSHCI_UPIU_TRANSACTION_CODE_NOP_OUT:
+ if (header->ehs_length != 0)
+ return (EINVAL);
+ *req_size = sizeof(struct ufshci_nop_out_upiu);
+ *resp_size = sizeof(struct ufshci_nop_in_upiu);
+ *is_admin = true;
+ max_data_len = 0;
+ break;
+ case UFSHCI_UPIU_TRANSACTION_CODE_COMMAND:
+ *req_size = sizeof(struct ufshci_cmd_command_upiu) + ehs_bytes;
+ /* The response carries an EHS too, for advanced RPMB. */
+ *resp_size = sizeof(struct ufshci_cmd_response_upiu) +
+ ehs_bytes;
+ *is_admin = false;
+ /* A command UPIU moves its payload through the PRDT. */
+ max_data_len = 0;
+ break;
+ default:
+ return (EINVAL);
+ }
+
+ /*
+ * The descriptor has no request length, so the controller takes it
+ * from the UPIU header. A header that declares more than the UPIU
+ * has room for would read past the command descriptor.
+ */
+ if (be16toh(header->data_segment_length) > max_data_len)
+ return (EINVAL);
+
+ return (0);
+}
+
+static int
+ufshci_passthrough_cmd(struct ufshci_controller *ctrlr,
+ struct ufshci_pt_command *pt)
+{
+ struct ufshci_completion_poll_status status;
+ struct ufshci_request *req;
+ size_t req_size, resp_size;
+ void *buf = NULL;
+ bool is_admin;
+ int error;
+
+ /*
+ * An aborted request is completed by hand and may leave cpl
+ * unfilled, so stack bytes would go back to userland.
+ */
+ memset(&status, 0, sizeof(status));
+
+ if (pt->timeout_ms != 0)
+ return (EINVAL);
+
+ /*
+ * The response copy below is shorter than this field, so without
+ * clearing it the tail would hold the caller's own input.
+ */
+ memset(&pt->resp_upiu, 0, sizeof(pt->resp_upiu));
+
+ /*
+ * The ABI cap is fixed. What a controller can actually map is not,
+ * so check both.
+ */
+ if (pt->len > UFSHCI_PT_MAX_XFER || pt->len > ctrlr->max_xfer_size)
+ return (EINVAL);
+ if (pt->len != 0 && pt->buf == NULL)
+ return (EINVAL);
+
+ /* A buffer with no direction would build a PRDT nothing reads. */
+ if (pt->len != 0 && (pt->flags &
+ (UFSHCI_PT_FLAG_DATA_IN | UFSHCI_PT_FLAG_DATA_OUT)) == 0)
+ return (EINVAL);
+
+ if ((pt->flags & (UFSHCI_PT_FLAG_DATA_IN | UFSHCI_PT_FLAG_DATA_OUT)) ==
+ (UFSHCI_PT_FLAG_DATA_IN | UFSHCI_PT_FLAG_DATA_OUT))
+ return (EINVAL);
+
+ error = ufshci_passthrough_upiu_sizes(pt, &req_size, &resp_size,
+ &is_admin);
+ if (error)
+ return (error);
+ if (req_size > sizeof(struct ufshci_upiu))
+ return (EINVAL);
+ /*
+ * The completion union is smaller than a UPIU. An EHS must not
+ * push the response past it.
+ */
+ if (resp_size > sizeof(status.cpl.response_upiu))
+ return (EINVAL);
+ /*
+ * Without EHSLUTRDS the controller cannot carry the EHS, so the
+ * device would answer a partial request.
+ */
+ if (pt->req_upiu.header.ehs_length != 0 &&
+ UFSHCIV(UFSHCI_CAP_REG_EHSLUTRDS, ctrlr->cap) == 0)
+ return (EOPNOTSUPP);
+
+ if (pt->len != 0) {
+ buf = malloc(pt->len, M_UFSHCI, M_WAITOK | M_ZERO);
+ if (pt->flags & UFSHCI_PT_FLAG_DATA_OUT) {
+ error = copyin(pt->buf, buf, pt->len);
+ if (error)
+ goto out;
+ }
+ }
+
+ req = ufshci_allocate_request_vaddr(buf, pt->len, M_WAITOK,
+ ufshci_completion_poll_cb, &status);
+
+ memcpy(&req->request_upiu, &pt->req_upiu, sizeof(req->request_upiu));
+ req->request_size = req_size;
+ req->response_size = resp_size;
+ req->is_admin = is_admin;
+
+ if (pt->flags & UFSHCI_PT_FLAG_DATA_OUT)
+ req->data_direction = UFSHCI_DATA_DIRECTION_FROM_SYS_TO_TGT;
+ else if (pt->flags & UFSHCI_PT_FLAG_DATA_IN)
+ req->data_direction = UFSHCI_DATA_DIRECTION_FROM_TGT_TO_SYS;
+ else
+ req->data_direction = UFSHCI_DATA_DIRECTION_NO_DATA_TRANSFER;
+
+ error = ufshci_ctrlr_submit_transfer_request(ctrlr, req);
+ if (error) {
+ ufshci_free_request(req);
+ goto out;
+ }
+
+ ufshci_completion_poll(&status);
+
+ memcpy(&pt->resp_upiu, &status.cpl.response_upiu,
+ min(sizeof(pt->resp_upiu), sizeof(status.cpl.response_upiu)));
+ pt->xfer_len = pt->len;
+ /* The completion does not carry the overall command status yet. */
+ pt->ocs = 0;
+
+ /*
+ * A refused command still answered, so let the caller read the
+ * response. A failure that left the response zeroed had no answer.
+ */
+ if (status.error && pt->resp_upiu.header.response == 0) {
+ error = EIO;
+ goto out;
+ }
+
+ if (pt->len != 0 && (pt->flags & UFSHCI_PT_FLAG_DATA_IN))
+ error = copyout(buf, pt->buf, pt->len);
+
+out:
+ free(buf, M_UFSHCI);
+ return (error);
+}
+
+static int
+ufshci_passthrough_uic_cmd(struct ufshci_controller *ctrlr,
+ struct ufshci_pt_uic_command *pt)
+{
+ uint32_t return_value = 0;
+ int error;
+
+ if (pt->timeout_ms != 0)
+ return (EINVAL);
+
+ /*
+ * Only the four attribute commands. The rest can drop the link or
+ * power the device off.
+ */
+ switch (pt->cmd.opcode) {
+ case UFSHCI_DME_GET:
+ case UFSHCI_DME_SET:
+ case UFSHCI_DME_PEER_GET:
+ case UFSHCI_DME_PEER_SET:
+ break;
+ default:
+ return (EINVAL);
+ }
+
+ /*
+ * On a path that never reads the register, a value the caller left
+ * here would read as an answer.
+ */
+ pt->cmd.argument2 &= ~(UFSHCI_UICCMDARG2_REG_ERROR_CODE_MASK <<
+ UFSHCI_UICCMDARG2_REG_ERROR_CODE_SHIFT);
+
+ error = ufshci_uic_send_cmd(ctrlr, &pt->cmd, &return_value);
+
+ pt->result = UFSHCIV(UFSHCI_UICCMDARG2_REG_ERROR_CODE,
+ pt->cmd.argument2);
+ if (pt->result != 0) {
+ /*
+ * A refusal is a result, not a transport failure. There is no
+ * attribute value to go with it, and the caller's own input
+ * would read like one.
+ */
+ pt->cmd.argument3 = 0;
+ return (0);
+ }
+ if (error)
+ return (error);
+
+ pt->cmd.argument3 = return_value;
+
+ return (0);
+}
+
static int
ufshci_ioctl(struct cdev *cdev, u_long cmd, caddr_t arg, int flag,
struct thread *td)
{
+ struct ufshci_controller *ctrlr = cdev->si_drv1;
+
switch (cmd) {
+ case UFSHCI_PASSTHROUGH_CMD:
+ return (ufshci_passthrough_cmd(ctrlr,
+ (struct ufshci_pt_command *)arg));
+ case UFSHCI_PASSTHROUGH_UIC:
+ return (ufshci_passthrough_uic_cmd(ctrlr,
+ (struct ufshci_pt_uic_command *)arg));
default:
return (ENOTTY);
}
diff --git a/sys/dev/ufshci/ufshci_ioctl.h b/sys/dev/ufshci/ufshci_ioctl.h
index 57e1ce6fa869..c545c073fc0a 100644
--- a/sys/dev/ufshci/ufshci_ioctl.h
+++ b/sys/dev/ufshci/ufshci_ioctl.h
@@ -31,23 +31,29 @@
* a command UPIU uses it.
*/
struct ufshci_pt_command {
+ /* The driver overwrites task_tag. */
struct ufshci_upiu req_upiu; /* [in] */
struct ufshci_upiu resp_upiu; /* [out] */
void *buf; /* [in] PRDT payload, may be NULL */
uint32_t len; /* [in] length of buf */
uint32_t flags; /* [in] UFSHCI_PT_FLAG_* */
- uint32_t timeout_ms; /* [in] 0 means the driver default */
- uint32_t xfer_len; /* [out] bytes actually moved */
- uint8_t ocs; /* [out] overall command status */
+ uint32_t timeout_ms; /* [in] reserved, must be 0 */
+ uint32_t xfer_len; /* [out] bytes the request carried */
+ uint8_t ocs; /* [out] reserved, always 0 for now */
uint8_t reserved[7];
};
struct ufshci_pt_uic_command {
struct ufshci_uic_cmd cmd; /* [in/out] opcode and argument1..3 */
- uint32_t timeout_ms; /* [in] */
+ uint32_t timeout_ms; /* [in] reserved, must be 0 */
uint32_t result; /* [out] UICCMDARG2 result code */
};
+/*
+ * A zero return means the command reached the device, not that the device
+ * accepted it. The response field in the response UPIU header carries
+ * that. On an errno no output field is filled.
+ */
#define UFSHCI_PASSTHROUGH_CMD _IOWR('u', 0, struct ufshci_pt_command)
#define UFSHCI_PASSTHROUGH_UIC _IOWR('u', 1, struct ufshci_pt_uic_command)
diff --git a/sys/dev/ufshci/ufshci_private.h b/sys/dev/ufshci/ufshci_private.h
index 846d7a355d82..66cbe4a81bb5 100644
--- a/sys/dev/ufshci/ufshci_private.h
+++ b/sys/dev/ufshci/ufshci_private.h
@@ -547,6 +547,8 @@ int ufshci_req_sdb_get_inflight_io(struct ufshci_controller *ctrlr);
int ufshci_uic_power_mode_ready(struct ufshci_controller *ctrlr);
int ufshci_uic_hibernation_ready(struct ufshci_controller *ctrlr);
int ufshci_uic_cmd_ready(struct ufshci_controller *ctrlr);
+int ufshci_uic_send_cmd(struct ufshci_controller *ctrlr,
+ struct ufshci_uic_cmd *uic_cmd, uint32_t *return_value);
int ufshci_uic_send_dme_link_startup(struct ufshci_controller *ctrlr);
int ufshci_uic_send_dme_get(struct ufshci_controller *ctrlr, uint16_t attribute,
uint32_t *return_value);
diff --git a/sys/dev/ufshci/ufshci_uic_cmd.c b/sys/dev/ufshci/ufshci_uic_cmd.c
index 45ded2a120c6..54e90253ec0b 100644
--- a/sys/dev/ufshci/ufshci_uic_cmd.c
+++ b/sys/dev/ufshci/ufshci_uic_cmd.c
@@ -165,7 +165,7 @@ ufshci_uic_wait_cmd(struct ufshci_controller *ctrlr,
return (0);
}
-static int
+int
ufshci_uic_send_cmd(struct ufshci_controller *ctrlr,
struct ufshci_uic_cmd *uic_cmd, uint32_t *return_value)
{
@@ -190,8 +190,9 @@ ufshci_uic_send_cmd(struct ufshci_controller *ctrlr,
/* The result registers stay valid only until the next command. */
if (error == 0) {
+ uic_cmd->argument2 = ufshci_mmio_read_4(ctrlr, ucmdarg2);
config_result_code = UFSHCIV(UFSHCI_UICCMDARG2_REG_ERROR_CODE,
- ufshci_mmio_read_4(ctrlr, ucmdarg2));
+ uic_cmd->argument2);
result_value = ufshci_mmio_read_4(ctrlr, ucmdarg3);
}