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

From: fQwQf

Date: Sat Oct 10 2026 - 13:01:14 EST


Keep fragment lengths separate from complete HID report lengths.
Assemble DATA reports into a dedicated buffer, enforce the declared
content length and final padding, and reject fragmented command
responses. Discard incomplete reports after a one-second gap or during
error recovery and suspend.

Require a body header before parsing and bound allocations to 8 KiB.
Preserve the v5 request serialization and response matching.

Use the existing max_fragment_length descriptor field for each fragment;
max_input_length describes a complete input report and remains distinct.

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

diff --git a/drivers/hid/spi-hid/spi-hid-core.c b/drivers/hid/spi-hid/spi-hid-core.c
index 9d02c156fcd641e9c944c0dfe08b5aad57a97f6a..2778d147b202ab663928a851198eca8fe628b16a 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);
@@ -335,6 +334,7 @@ static int spi_hid_suspend(struct spi_hid *shid)
*/
disable_irq(shid->spi->irq);
cancel_work_sync(&shid->reset_work);
+ shid->fragment_len = 0;

guard(mutex)(&shid->power_lock);
if (!device_may_wakeup(dev)) {
@@ -468,6 +468,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");
@@ -1039,6 +1040,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)
{
@@ -1061,24 +1104,20 @@ static int spi_hid_bus_validate_header(struct spi_hid *shid,
* report_length is device-provided and is used as the size of the body
* transfer, so it must never exceed the input buffer.
*/
- if (header->report_length > max_body) {
+ if (header->report_length < HIDSPI_INPUT_BODY_HEADER_SIZE ||
+ header->report_length > max_body) {
dev_err(dev, "Input report too big: %u > %u\n",
header->report_length, max_body);
return -EMSGSIZE;
}

- if (shid->desc.max_input_length != 0 &&
- header->report_length > shid->desc.max_input_length) {
+ if (shid->desc.max_fragment_length &&
+ header->report_length > shid->desc.max_fragment_length) {
dev_err(dev, "Input report body size %u > max expected of %u\n",
- header->report_length, shid->desc.max_input_length);
+ header->report_length, shid->desc.max_fragment_length);
return -EMSGSIZE;
}

- if (header->last_fragment_flag != 1) {
- dev_err(dev, "Multi-fragment reports not supported\n");
- return -EOPNOTSUPP;
- }
-
if (header->sync_const != SPI_HID_INPUT_HEADER_SYNC_BYTE) {
dev_err(dev, "Invalid input report sync constant (0x%x)\n",
header->sync_const);
@@ -1149,6 +1188,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);

@@ -1231,7 +1271,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);
@@ -1249,6 +1289,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;
@@ -1264,6 +1307,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 554288dd17631edc1a503755b04170c04501bcb7..635d66daaf456cdb65c57146758ea7c64a1537f7 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;