mfd: qnap-mcu: keep the reply buffer alive past a command timeout
Subsystems (3)
PATCH v2 1/1 mfd: qnap-mcu: keep the reply buffer alive past a command timeout
qnap_mcu_exec() publishes an on-stack buffer to the receive path:
unsigned char rx[QNAP_MCU_RX_BUFFER_SIZE]; ... reply->data = rx; reply->length = length;
and qnap_mcu_receive_buf() writes into it from the serdev receive path, which runs out of flush_to_ldisc() and is not serialized against qnap_mcu_exec() at all. bus_lock cannot cover it, because qnap_mcu_exec() holds that mutex across wait_for_completion_timeout().
On a timeout qnap_mcu_exec() returns with reply->data still pointing at its own frame. A reply that arrives late, or an unsolicited message from the MCU, is then written into a stack frame that has been left, corrupting whatever runs next on that stack. The same applies when qnap_mcu_write() fails, since that path returns without touching the reply state either.
Move the receive buffer into struct qnap_mcu. It is 37 bytes and the structure is devm_kzalloc()ed, so it lives as long as the driver, and a late write lands in memory that is still valid and is reinitialized by the next command. bus_lock keeps commands from sharing it.
This deliberately does not clear reply->data or reply->length on the timeout path. Doing so races with qnap_mcu_receive_buf(), which reads both after its
if (!reply->length) return size;
check: clearing reply->data gives a NULL dereference, and clearing reply->length alone removes the reply->received == reply->length exit condition, so the copy loop runs until the uart chunk is consumed and overruns the buffer. Leaving both set keeps the write bounded by reply->length, which qnap_mcu_exec() has already checked against sizeof(mcu->rx).
---
v1 cleared reply->length and reply->data on the timeout path. That was
wrong: it races with qnap_mcu_receive_buf() and, as sashiko-bot pointed out on that thread, can give a NULL dereference or drop the copy loop's exit condition and overrun the buffer. It made one failure mode worse than the one it fixed. Thanks to the bot for catching it.
https://lore.kernel.org/all/[email protected]/
v2 leaves the reply state alone and gives the buffer a lifetime instead, so there is nothing to tear down and no new race.
What this does not fix, and what I am not proposing to fix here: a late reply can still be written into the buffer while the next command is using it, so it can corrupt that command's data or complete it early. The checksum test turns most of that into -EPROTO rather than bad data reaching the caller. Fixing it properly means serializing reply setup and teardown against qnap_mcu_receive_buf(), which is a change to the driver's synchronisation model and not something I want to fold into a fix. Same for the unprotected reply->received accesses, which KCSAN would flag.
I have no QNAP hardware, so this is from reading the driver rather than from an observed corruption. What I checked:
- flush_to_ldisc() calls receive_buf() from a workqueue, so it is process context and genuinely concurrent with qnap_mcu_exec()
- bus_lock is held across wait_for_completion_timeout(), so qnap_mcu_receive_buf() cannot take it
- struct qnap_mcu comes from devm_kzalloc() in qnap_mcu_probe()
- length is checked against sizeof(mcu->rx) before it is published, so the bounded write stays inside the buffer
- the u8 rx[14] in qnap_mcu_get_version() is a caller buffer, filled by memcpy() under bus_lock after the reply is complete, so it is not exposed to the receive path and is left alone
drivers/mfd/qnap-mcu.c | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-)
drivers/mfd/qnap-mcu.c +7 -6
@@ -56,6 +56,7 @@ struct qnap_mcu_reply {* @reply: Reply data structure * @variant: Device variant specific information * @version: MCU firmware version +* @rx: Receive buffer the reply is assembled in */ struct qnap_mcu {struct serdev_device *serdev;
@@ -63,6 +64,7 @@ struct qnap_mcu {struct qnap_mcu_reply reply;const struct qnap_mcu_variant *variant;u8 version[QNAP_MCU_VERSION_LEN ]; +u8 rx[QNAP_MCU_RX_BUFFER_SIZE ]; };/*
@@ -214,19 +216,18 @@ int qnap_mcu_exec(struct qnap_mcu *mcu, const u8 *cmd_data, size_t cmd_data_size, u8 *reply_data, size_t reply_data_size) { - unsigned char rx[QNAP_MCU_RX_BUFFER_SIZE]; size_t length = reply_data_size + QNAP_MCU_CHECKSUM_SIZE;struct qnap_mcu_reply *reply = &mcu->reply;int ret =0 ; -if (length >sizeof (rx)) { +if (length >sizeof (mcu->rx)) {dev_err (&mcu->serdev->dev,"expected data too big for receive buffer" );return -EINVAL ; }guard (mutex)(&mcu->bus_lock); - reply->data = rx; + reply->data = mcu->rx; reply->length = length; reply->received =0 ;reinit_completion (&reply->done);
@@ -242,15 +243,15 @@ int qnap_mcu_exec(struct qnap_mcu *mcu,return -ETIMEDOUT ; } -if (!qnap_mcu_verify_checksum (rx, reply->received)) { +if (!qnap_mcu_verify_checksum (mcu->rx, reply->received)) {dev_err (&mcu->serdev->dev,"Invalid Checksum received from controller\n" );return -EPROTO ; } -if (qnap_mcu_reply_is_any_error (mcu, rx, reply->received)) +if (qnap_mcu_reply_is_any_error (mcu, mcu->rx, reply->received))return -EPROTO ; -memcpy (reply_data, rx, reply_data_size); +memcpy (reply_data, mcu->rx, reply_data_size);return 0 ; }
base-commit: 2d2338c93da79b3bfe4b6099a931d9468d539952
Signature · 1 line
2.55.0
[email protected]
AI reviewThank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
New issues:
- [Low] The commit message description is not capitalized, violating MFD subsystem formatting rules.
Pre-existing issues:
- [Critical] A data race between qnap_mcu_exec() and qnap_mcu_receive_buf() on reply->length and reply->received bypasses the loop exit condition, leading to a severe heap buffer overflow.
- [High] Late UART replies from timed-out or failed commands can be written into the active buffer of the next command, causing data cross-talk and spurious early completion.
- [High] A data race can cause qnap_mcu_verify_checksum() to be called with reply->received == 0, leading to an integer underflow and a deterministic kernel panic. --
--- Patch [1]: [PATCH v2] mfd: qnap-mcu: keep the reply buffer alive past a command timeout --- commit 876c0f2fd3f8e4b677ad50b82d00f3dcfe4a3283
Author: Ali Ahmet Memis <[email protected]>
mfd: qnap-mcu: keep the reply buffer alive past a command timeout
This patch moves the receive buffer from the stack in qnap_mcu_exec() to the qnap_mcu structure. This prevents stack corruption when late replies arrive after a command has timed out.
Quoted 1 line
mfd: qnap-mcu: keep the reply buffer alive past a command timeout
[Severity: Low]
Is there a capitalization typo in the subject line? Should "keep" be "Keep"?
Quoted 4 lines
drivers/mfd/qnap-mcu.c +0
-0
[ ... ]
Quoted 11 lines
drivers/mfd/qnap-mcu.c +1
-2@@ -214,19 +216,18 @@ int qnap_mcu_exec(struct qnap_mcu *mcu, const u8 *cmd_data, size_t cmd_data_size, u8 *reply_data, size_t reply_data_size) { - unsigned char rx[QNAP_MCU_RX_BUFFER_SIZE]; size_t length = reply_data_size + QNAP_MCU_CHECKSUM_SIZE;struct qnap_mcu_reply *reply = &mcu->reply;int ret =0 ; -if (length >sizeof (rx)) { +if (length >sizeof (mcu->rx)) {
drivers/mfd/qnap-mcu.c int qnap_mcu_exec(struct qnap_mcu *mcu, · line 232
Quoted 10 lines
drivers/mfd/qnap-mcu.c +1
-1dev_err (&mcu->serdev->dev,"expected data too big for receive buffer" );return -EINVAL ; }guard (mutex)(&mcu->bus_lock); - reply->data = rx; + reply->data = mcu->rx; reply->length = length; reply->received =0 ;
[Severity: Critical]
This is a pre-existing issue, but can this reset race with an active receive loop and cause a heap buffer overflow?
If the receive thread is running in qnap_mcu_receive_buf() and we reset reply->received to 0 here without synchronization, the exact equality check in the receive thread could be bypassed:
drivers/mfd/qnap-mcu.c:qnap_mcu_receive_buf()
reply->received++;
if (reply->received == reply->length) {
...If the equality check is bypassed, the receive loop could write unbounded bytes into reply->data (which now points to mcu->rx on the heap), causing a heap buffer overflow.
drivers/mfd/qnap-mcu.c int qnap_mcu_exec(struct qnap_mcu *mcu, · line 233
Quoted 1 line
drivers/mfd/qnap-mcu.c +0
-0reinit_completion (&reply->done);
[Severity: High]
This isn't a bug introduced by this patch, but can resetting the completion here allow late replies to corrupt the new command?
If a previous command times out, its receive thread is still active.
If we just reset the completion and start a new command, could a concurrently arriving late response from the previous command be written into mcu->rx by qnap_mcu_receive_buf()?
Since the old response is well-formed, it could pass the simple XOR checksum, triggering a spurious complete() and causing the new command to instantly return with stale data.
Quoted 4 lines
drivers/mfd/qnap-mcu.c +0
-0ret =qnap_mcu_write (mcu, cmd_data, cmd_data_size);if (ret <0 )return ret;
drivers/mfd/qnap-mcu.c
Quoted 10 lines
drivers/mfd/qnap-mcu.c +1
-1serdev_device_wait_until_sent (mcu->serdev,msecs_to_jiffies (QNAP_MCU_TIMEOUT_MS ));if (!wait_for_completion_timeout (&reply->done,msecs_to_jiffies (QNAP_MCU_TIMEOUT_MS ))) {dev_err (&mcu->serdev->dev,"Command timeout\n" );return -ETIMEDOUT ; } -if (!qnap_mcu_verify_checksum (rx, reply->received)) { +if (!qnap_mcu_verify_checksum (mcu->rx, reply->received)) {
[Severity: High]
This is a pre-existing issue, but could this cause an integer underflow and a kernel panic if reply->received is 0?
If qnap_mcu_receive_buf() evaluates reply->received == reply->length and zeroes reply->length, but is preempted before calling complete(), the original command can time out.
A new command could then call qnap_mcu_exec(), setting reply->received to 0.
If the preempted receive thread then resumes and calls complete(), the new command will wake up and call qnap_mcu_verify_checksum() with reply->received == 0:
drivers/mfd/qnap-mcu.c:qnap_mcu_verify_checksum()
u8 crc = qnap_mcu_csum(buf, size - QNAP_MCU_CHECKSUM_SIZE);
If size is 0, this underflows to the maximum size value, and qnap_mcu_csum() will loop reading sequentially out of bounds until it hits an unmapped page.
Quoted 3 lines
drivers/mfd/qnap-mcu.c +0
-0dev_err (&mcu->serdev->dev,"Invalid Checksum received from controller\n" );return -EPROTO ; }
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=1
Re: (subset) [PATCH v2] mfd: qnap-mcu: keep the reply buffer alive past a command timeout
In reply to Ali Ahmet MemisQuoted 8 lines · Ali Ahmet Memis
qnap_mcu_exec()publishes an on-stack buffer to the receive path:unsigned char rx[QNAP_MCU_RX_BUFFER_SIZE]; ... reply->data = rx; reply->length = length;[...]
Applied, thanks!
[1/1] mfd: qnap-mcu: keep the reply buffer alive past a command timeout
commit: 47504742cea7878ebd1bf1491bbed923df6b90b1
--
Lee Jones [李琼斯]