[RFC PATCH v3 3/5] HID: spi-hid: assemble bounded fragmented input reports

From: Jizhou Tong

Date: Thu Nov 05 2026 - 14:24:00 EST


The driver rejects every report split across multiple wire fragments.
Assemble DATA fragments before passing a complete report to the HID
layer, checking the declared length, final padding and inter-fragment
timeout. Command responses remain atomic.

Limit report_size to 8 KiB before allocating buffers and discard partial
reports on suspend and recovery. Preserve minimum-body validation and
check each wire fragment against max_fragment_length independently of
the complete input-report size.

Signed-off-by: Jizhou Tong <fqwqf@xxxxxxxxx>
---
drivers/hid/spi-hid/spi-hid-core.c | 64 +++++++++++++++++++++++++++---
drivers/hid/spi-hid/spi-hid-core.h | 4 ++
2 files changed, 63 insertions(+), 5 deletions(-)

diff --git a/drivers/hid/spi-hid/spi-hid-core.c b/drivers/hid/spi-hid/spi-hid-core.c
index 53da68c705..ef5fb11f23 100644
--- a/drivers/hid/spi-hid/spi-hid-core.c
+++ b/drivers/hid/spi-hid/spi-hid-core.c
@@ -146,7 +146,6 @@ static void spi_hid_parse_dev_desc(const struct hidspi_dev_descriptor *raw,
desc->max_input_length = le16_to_cpu(raw->max_input_len);
desc->max_output_length = le16_to_cpu(raw->max_output_len);

- /* FIXME: multi-fragment not supported, field below not used */
desc->max_fragment_length = le16_to_cpu(raw->max_frag_len);

desc->vendor_id = le16_to_cpu(raw->vendor_id);
@@ -330,6 +329,7 @@ static int spi_hid_suspend(struct spi_hid *shid)

/* disable_irq() waits for a running IRQ thread. */
disable_irq(shid->spi->irq);
+ shid->fragment_len = 0;

guard(mutex)(&shid->power_lock);
if (!device_may_wakeup(dev)) {
@@ -461,6 +461,7 @@ static void spi_hid_error_handler(struct spi_hid *shid)
return;

disable_irq(shid->spi->irq);
+ shid->fragment_len = 0;

if (shid->reset_attempts++ >= SPI_HID_MAX_RESET_ATTEMPTS) {
dev_err(dev, "unresponsive device, aborting\n");
@@ -1035,6 +1036,48 @@ static int spi_hid_process_input_report(struct spi_hid *shid,
return 0;
}

+/* Only DATA reports may be fragmented; command responses remain atomic. */
+static int spi_hid_accept_fragment(struct spi_hid *shid,
+ const struct spi_hid_input_header *header)
+{
+ struct spi_hid_input_report body;
+ u16 len = header->report_length;
+ unsigned int total;
+ int error;
+
+ if (shid->fragment_len &&
+ time_after(jiffies, shid->fragment_time + msecs_to_jiffies(1000)))
+ goto invalid;
+ if (!shid->fragment_len) {
+ if (header->last_fragment_flag)
+ return spi_hid_process_input_report(shid, shid->input);
+ spi_hid_populate_input_body(shid->input->body, &body);
+ if (body.report_type != DATA || body.content_length > shid->bufsize)
+ goto invalid;
+ shid->fragment_expected = HIDSPI_INPUT_BODY_SIZE(body.content_length);
+ }
+ total = shid->fragment_len + len;
+ if (total > round_up(shid->bufsize + HIDSPI_INPUT_BODY_HEADER_SIZE, 4) ||
+ (!header->last_fragment_flag && total >= shid->fragment_expected) ||
+ (header->last_fragment_flag && total != ALIGN(shid->fragment_expected, 4)))
+ goto invalid;
+ memcpy((u8 *)shid->fragment + offsetof(struct spi_hid_input_buf, body) +
+ shid->fragment_len, shid->input->body, len);
+ shid->fragment_len = total;
+ shid->fragment_time = jiffies;
+ if (!header->last_fragment_flag)
+ return 0;
+
+ memcpy(shid->fragment->header, shid->input->header, HIDSPI_INPUT_HEADER_SIZE);
+ put_unaligned_le16((total / 4) | BIT(14), &shid->fragment->header[1]);
+ error = spi_hid_process_input_report(shid, shid->fragment);
+ shid->fragment_len = 0;
+ return error;
+invalid:
+ shid->fragment_len = 0;
+ return -EPROTO;
+}
+
static int spi_hid_bus_validate_header(struct spi_hid *shid,
struct spi_hid_input_header *header)
{
@@ -1070,9 +1113,11 @@ static int spi_hid_bus_validate_header(struct spi_hid *shid,
return -EMSGSIZE;
}

- if (header->last_fragment_flag != 1) {
- dev_err(dev, "Multi-fragment reports not supported\n");
- return -EOPNOTSUPP;
+ if (shid->desc.max_fragment_length &&
+ header->report_length > shid->desc.max_fragment_length) {
+ dev_err(dev, "Input fragment size %u > max expected of %u\n",
+ header->report_length, shid->desc.max_fragment_length);
+ return -EMSGSIZE;
}

if (header->sync_const != SPI_HID_INPUT_HEADER_SYNC_BYTE) {
@@ -1152,6 +1197,7 @@ static int spi_hid_set_request(struct spi_hid *shid, u8 *arg_buf, u16 arg_len,
/* Schedule a reset to recover from an error in the IRQ handler. */
static irqreturn_t spi_hid_irq_error(struct spi_hid *shid)
{
+ shid->fragment_len = 0;
set_bit(SPI_HID_ERROR, &shid->flags);
schedule_work(&shid->reset_work);

@@ -1228,7 +1274,7 @@ static irqreturn_t spi_hid_dev_irq(int irq, void *_shid)
}
}

- error = spi_hid_process_input_report(shid, shid->input);
+ error = spi_hid_accept_fragment(shid, &header);
if (error) {
dev_err(dev, "Failed to process input report: %d\n", error);
return spi_hid_irq_error(shid);
@@ -1246,6 +1292,9 @@ static int spi_hid_alloc_buffers(struct spi_hid *shid, size_t report_size)
size_t xfer_size;
void *tmp;

+ if (report_size > SZ_8K)
+ return -EMSGSIZE;
+
tmp = devm_krealloc(dev, shid->output, outbufsize, GFP_KERNEL | __GFP_ZERO);
if (!tmp)
return -ENOMEM;
@@ -1261,6 +1310,11 @@ static int spi_hid_alloc_buffers(struct spi_hid *shid, size_t report_size)
return -ENOMEM;
shid->response = tmp;

+ tmp = devm_krealloc(dev, shid->fragment, inbufsize, GFP_KERNEL | __GFP_ZERO);
+ if (!tmp)
+ return -ENOMEM;
+ shid->fragment = tmp;
+
/* io_lock serializes the separate, DMA-aligned TX and RX regions. */
xfer_size = ALIGN(max(inbufsize, outbufsize), ARCH_DMA_MINALIGN) +
ARCH_DMA_MINALIGN;
diff --git a/drivers/hid/spi-hid/spi-hid-core.h b/drivers/hid/spi-hid/spi-hid-core.h
index 768d900bc7..a65a764d0b 100644
--- a/drivers/hid/spi-hid/spi-hid-core.h
+++ b/drivers/hid/spi-hid/spi-hid-core.h
@@ -61,6 +61,10 @@ struct spi_hid {
struct spi_hid_output_buf *output; /* Output buffer. */
struct spi_hid_input_buf *input; /* Input buffer. */
struct spi_hid_input_buf *response; /* Response buffer. */
+ struct spi_hid_input_buf *fragment;
+ u16 fragment_len;
+ u16 fragment_expected;
+ unsigned long fragment_time;

struct drm_panel_follower panel_follower;
bool is_panel_follower;