* [PATCH v2 0/2] Couple of SCMI race fixes
@ 2026-09-09 4:32 Roland Dreier via B4 Relay
2026-09-09 4:32 ` [PATCH v2 1/2] firmware: arm_scmi: Protect xfer->async_done with xfer->lock Roland Dreier via B4 Relay
2026-09-09 4:32 ` [PATCH v2 2/2] firmware: arm_scmi: Don't reuse raw xfers with async_done still armed Roland Dreier via B4 Relay
0 siblings, 2 replies; 5+ messages in thread
From: Roland Dreier via B4 Relay @ 2026-09-09 4:32 UTC (permalink / raw)
To: Sudeep Holla, Cristian Marussi
Cc: arm-scmi, linux-arm-kernel, linux-kernel, Roland Dreier
Noticed a couple of races in SCMI code around delayed responses that
time out. These are probably nearly impossible to hit in practice but
the fixes are pretty straightforward and it seems worth having the
code be sound.
The v1 fixes were correct as posted, despite Sashiko warning that the
new "delayed_response_dropped" debug counter might never increment.
However, that review made me think that a new debug counter for a
one-in-a-gazillion race condition is probably less useful than a
kernel error message. So the change in v2 is having the first patch
log an error for a delayed response that arrives in the race window it
closes (matching the existing "Delayed Response not expected" error in
scmi_msg_response_validate()). The new error is expected to be
extremely rare but it makes sure there is no path where a delayed
response gets matched and then discarded without any logging.
Signed-off-by: Roland Dreier <rolanddreier@rivian.com>
---
Changes in v2:
- Patch 1: replace the new delayed_response_dropped debugfs counter
with a dev_err() log message, since the condition is rare enough
that logging is more useful than counting (per sashiko review of v1).
- Link to v1: https://patch.msgid.link/20260814-scmi-async-done-race-v1-0-335b163d77ee@rivian.com
To: Sudeep Holla <sudeep.holla@kernel.org>
To: Cristian Marussi <cristian.marussi@arm.com>
Cc: arm-scmi@vger.kernel.org
Cc: linux-arm-kernel@lists.infradead.org
Cc: linux-kernel@vger.kernel.org
---
Roland Dreier (2):
firmware: arm_scmi: Protect xfer->async_done with xfer->lock
firmware: arm_scmi: Don't reuse raw xfers with async_done still armed
drivers/firmware/arm_scmi/common.h | 22 ++++++++++++++++++++
drivers/firmware/arm_scmi/driver.c | 38 ++++++++++++++++++++++++++++++-----
drivers/firmware/arm_scmi/protocols.h | 9 ++++++---
drivers/firmware/arm_scmi/raw_mode.c | 29 ++++++++++++++++++++------
4 files changed, 84 insertions(+), 14 deletions(-)
---
base-commit: 786262be6048deab760f68c8acc2c85607165894
change-id: 20260814-scmi-async-done-race-29117fd1e9a5
Best regards,
--
Roland Dreier <rolanddreier@rivian.com>
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH v2 1/2] firmware: arm_scmi: Protect xfer->async_done with xfer->lock
2026-09-09 4:32 [PATCH v2 0/2] Couple of SCMI race fixes Roland Dreier via B4 Relay
@ 2026-09-09 4:32 ` Roland Dreier via B4 Relay
2026-09-09 11:18 ` Cristian Marussi
2026-09-09 4:32 ` [PATCH v2 2/2] firmware: arm_scmi: Don't reuse raw xfers with async_done still armed Roland Dreier via B4 Relay
1 sibling, 1 reply; 5+ messages in thread
From: Roland Dreier via B4 Relay @ 2026-09-09 4:32 UTC (permalink / raw)
To: Sudeep Holla, Cristian Marussi
Cc: arm-scmi, linux-arm-kernel, linux-kernel, Roland Dreier
From: Roland Dreier <rolanddreier@rivian.com>
Asynchronous SCMI commands are completed by a delayed response. The RX
path signals the response with complete(xfer->async_done). Unlike
xfer->done, xfer->async_done is a pointer to a completion owned by
whoever is waiting for the delayed response, and it stays valid only for
as long as that waiter is still waiting. In do_xfer_with_response() it
is a DECLARE_COMPLETION_ONSTACK() in the caller's stack frame.
Nothing serialises the RX path against a waiter that gives up on a
timeout. scmi_msg_response_validate() does read xfer->async_done
under xfer->lock, and documents that as a requirement, but the lock is
dropped again before scmi_handle_response() dereferences the pointer,
and neither the arming nor the disarming side takes it at all. So a
delayed response arriving just as the wait times out can be signalled
on a completion that is already gone:
waiter RX path (IRQ context)
------ ---------------------
do_xfer_with_response():
xfer->async_done = &async_response
do_xfer(xfer)
wait_for_completion_timeout(xfer->async_done, tmo)
/* returns 0, gives up */
/* response receive interrupt */
scmi_handle_response():
scmi_xfer_command_acquire()
lock xfer->lock
validate: async_done != NULL
unlock xfer->lock
xfer->async_done = NULL
return -ETIMEDOUT
/* async_response goes out of scope */
complete(xfer->async_done)
That last complete() has two possible bad outcomes: it either dereferences
the NULL just stored by the waiter or - if that store is not yet visible
on the RX CPU - it takes a lock and writes to a stack frame that the
waiter may already have returned from.
Fix this by making xfer->lock cover xfer->async_done end-to-end. Add
helpers to arm and disarm it under the lock, use them on both the regular
and the raw paths, and have the RX path read and signal the completion
under that same lock. A waiter that is timing out then either completes
its disarm before the RX path looks, in which case the delayed response is
dropped, or blocks in the disarm until the RX path is done with the
completion, in which case the completion is still alive.
Log the dropped case, so every path where a delayed response that was
matched to a pending xfer and then discarded produces an error. (This is
the same treatment scmi_msg_response_validate() already gives a
delayed response that arrives after the waiter has disarmed the xfer)
Fixes: 58ecdf03dbb9 ("firmware: arm_scmi: Add support for asynchronous commands and delayed response")
Signed-off-by: Roland Dreier <rolanddreier@rivian.com>
---
drivers/firmware/arm_scmi/common.h | 22 +++++++++++++++++++++
drivers/firmware/arm_scmi/driver.c | 37 ++++++++++++++++++++++++++++++-----
drivers/firmware/arm_scmi/protocols.h | 9 ++++++---
drivers/firmware/arm_scmi/raw_mode.c | 4 ++--
4 files changed, 62 insertions(+), 10 deletions(-)
diff --git a/drivers/firmware/arm_scmi/common.h b/drivers/firmware/arm_scmi/common.h
index fe8c22cfb9f7..a5492dd90b41 100644
--- a/drivers/firmware/arm_scmi/common.h
+++ b/drivers/firmware/arm_scmi/common.h
@@ -284,6 +284,28 @@ static inline bool is_polling_enabled(struct scmi_chan_info *cinfo,
is_transport_polling_capable(desc);
}
+/**
+ * scmi_xfer_async_response_arm - Arm the delayed response completion
+ *
+ * @xfer: A reference to the xfer to arm
+ * @async_done: The completion to signal upon reception of a delayed response,
+ * or NULL to disarm @xfer.
+ */
+static inline void scmi_xfer_async_response_arm(struct scmi_xfer *xfer,
+ struct completion *async_done)
+{
+ unsigned long flags;
+
+ spin_lock_irqsave(&xfer->lock, flags);
+ xfer->async_done = async_done;
+ spin_unlock_irqrestore(&xfer->lock, flags);
+}
+
+static inline void scmi_xfer_async_response_disarm(struct scmi_xfer *xfer)
+{
+ scmi_xfer_async_response_arm(xfer, NULL);
+}
+
void scmi_xfer_raw_put(const struct scmi_handle *handle,
struct scmi_xfer *xfer);
struct scmi_xfer *scmi_xfer_raw_get(const struct scmi_handle *handle);
diff --git a/drivers/firmware/arm_scmi/driver.c b/drivers/firmware/arm_scmi/driver.c
index ef29fd223287..847c09da310e 100644
--- a/drivers/firmware/arm_scmi/driver.c
+++ b/drivers/firmware/arm_scmi/driver.c
@@ -1064,6 +1064,29 @@ static inline void scmi_xfer_command_release(struct scmi_info *info,
__scmi_xfer_put(&info->tx_minfo, xfer);
}
+/**
+ * scmi_xfer_async_response_complete - Signal a received delayed response
+ *
+ * @xfer: A reference to the xfer whose delayed response was received
+ *
+ * Return: True if a completion was still armed on @xfer and has been
+ * signalled, false if a waiter has already disarmed it due to
+ * timeout or other error.
+ */
+static bool scmi_xfer_async_response_complete(struct scmi_xfer *xfer)
+{
+ unsigned long flags;
+ struct completion *async_done;
+
+ spin_lock_irqsave(&xfer->lock, flags);
+ async_done = xfer->async_done;
+ if (async_done)
+ complete(async_done);
+ spin_unlock_irqrestore(&xfer->lock, flags);
+
+ return !!async_done;
+}
+
static inline void scmi_clear_channel(struct scmi_info *info,
struct scmi_chan_info *cinfo)
{
@@ -1167,8 +1190,12 @@ static void scmi_handle_response(struct scmi_chan_info *cinfo,
if (xfer->hdr.type == MSG_TYPE_DELAYED_RESP) {
scmi_clear_channel(info, cinfo);
- complete(xfer->async_done);
- scmi_inc_count(info->dbg, DELAYED_RESPONSE_OK);
+ if (scmi_xfer_async_response_complete(xfer))
+ scmi_inc_count(info->dbg, DELAYED_RESPONSE_OK);
+ else
+ dev_err(cinfo->dev,
+ "Delayed Response for %d (protocol 0x%X msg 0x%X) received after waiter was disarmed, dropping\n",
+ xfer->hdr.seq, xfer->hdr.protocol_id, xfer->hdr.id);
} else {
complete(&xfer->done);
scmi_inc_count(info->dbg, RESPONSE_OK);
@@ -1510,7 +1537,7 @@ static int do_xfer_with_response(const struct scmi_protocol_handle *ph,
int ret, timeout = msecs_to_jiffies(SCMI_MAX_RESPONSE_TIMEOUT);
DECLARE_COMPLETION_ONSTACK(async_response);
- xfer->async_done = &async_response;
+ scmi_xfer_async_response_arm(xfer, &async_response);
/*
* Delayed responses should not be polled, so an async command should
@@ -1522,7 +1549,7 @@ static int do_xfer_with_response(const struct scmi_protocol_handle *ph,
ret = do_xfer(ph, xfer);
if (!ret) {
- if (!wait_for_completion_timeout(xfer->async_done, timeout)) {
+ if (!wait_for_completion_timeout(&async_response, timeout)) {
dev_err(ph->dev,
"timed out in delayed resp(caller: %pS)\n",
(void *)_RET_IP_);
@@ -1532,7 +1559,7 @@ static int do_xfer_with_response(const struct scmi_protocol_handle *ph,
}
}
- xfer->async_done = NULL;
+ scmi_xfer_async_response_disarm(xfer);
return ret;
}
diff --git a/drivers/firmware/arm_scmi/protocols.h b/drivers/firmware/arm_scmi/protocols.h
index 15ad5162e37a..8583159059e6 100644
--- a/drivers/firmware/arm_scmi/protocols.h
+++ b/drivers/firmware/arm_scmi/protocols.h
@@ -100,7 +100,10 @@ struct scmi_msg_hdr {
* message. If request-ACK protocol is used, we can reuse the same
* buffer for the rx path as we use for the tx path.
* @done: command message transmit completion event
- * @async_done: pointer to delayed response message received event completion
+ * @async_done: pointer to delayed response message received event completion,
+ * or NULL when no delayed response is expected. Protected by
+ * @lock, since the completion is owned by the waiter and can
+ * vanish once the wait times out.
* @pending: True for xfers added to @pending_xfers hashtable
* @node: An hlist_node reference used to store this xfer, alternatively, on
* the free list @free_xfers or in the @pending_xfers hashtable
@@ -121,7 +124,7 @@ struct scmi_msg_hdr {
* - SCMI_XFER_SENT_OK -> SCMI_XFER_DRESP_OK
* (Missing synchronous response is assumed OK and ignored)
* @flags: Optional flags associated to this xfer.
- * @lock: A spinlock to protect state and busy fields.
+ * @lock: A spinlock to protect state, busy and async_done fields.
* @priv: A pointer for transport private usage.
*/
struct scmi_xfer {
@@ -147,7 +150,7 @@ struct scmi_xfer {
#define SCMI_XFER_IS_CHAN_SET(x) \
((x)->flags & SCMI_XFER_FLAG_CHAN_SET)
int flags;
- /* A lock to protect state and busy fields */
+ /* A lock to protect state, busy and async_done fields */
spinlock_t lock;
void *priv;
};
diff --git a/drivers/firmware/arm_scmi/raw_mode.c b/drivers/firmware/arm_scmi/raw_mode.c
index 1f6e51670208..8751cff5fa4e 100644
--- a/drivers/firmware/arm_scmi/raw_mode.c
+++ b/drivers/firmware/arm_scmi/raw_mode.c
@@ -346,7 +346,7 @@ scmi_xfer_raw_waiter_get(struct scmi_raw_mode_info *raw, struct scmi_xfer *xfer,
if (async) {
reinit_completion(&rw->async_response);
- xfer->async_done = &rw->async_response;
+ scmi_xfer_async_response_arm(xfer, &rw->async_response);
}
rw->cinfo = cinfo;
@@ -361,7 +361,7 @@ static void scmi_xfer_raw_waiter_put(struct scmi_raw_mode_info *raw,
struct scmi_xfer_raw_waiter *rw)
{
if (rw->xfer) {
- rw->xfer->async_done = NULL;
+ scmi_xfer_async_response_disarm(rw->xfer);
rw->xfer = NULL;
}
--
2.54.0
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH v2 2/2] firmware: arm_scmi: Don't reuse raw xfers with async_done still armed
2026-09-09 4:32 [PATCH v2 0/2] Couple of SCMI race fixes Roland Dreier via B4 Relay
2026-09-09 4:32 ` [PATCH v2 1/2] firmware: arm_scmi: Protect xfer->async_done with xfer->lock Roland Dreier via B4 Relay
@ 2026-09-09 4:32 ` Roland Dreier via B4 Relay
2026-09-09 11:19 ` Cristian Marussi
1 sibling, 1 reply; 5+ messages in thread
From: Roland Dreier via B4 Relay @ 2026-09-09 4:32 UTC (permalink / raw)
To: Sudeep Holla, Cristian Marussi
Cc: arm-scmi, linux-arm-kernel, linux-kernel, Roland Dreier
From: Roland Dreier <rolanddreier@rivian.com>
In SCMI raw mode, scmi_xfer_raw_worker() releases the xfer before
releasing the waiter that disarms xfer->async_done, and scmi_xfer_get()
does not clear async_done when it hands out a recycled xfer. A concurrent
transaction can therefore pick up the xfer while it still points at the
old waiter's completion, so:
- a delayed response arriving for the new transaction can be signalled
on the old waiter's completion, which may already be re-armed for yet
another unrelated transaction, making that transaction's wait return
early; and
- the old waiter's disarm, which still runs after the xfer has been
released, clobbers the arming just installed by the new transaction,
so the new waiter times out even if its delayed response arrives.
Release the waiter first, while the worker still holds a reference on
the xfer, so that an xfer can never reach the free list still
armed. Track whether a delayed response is expected in the waiter
itself instead of peeking at xfer->async_done outside xfer->lock, and
wait on the waiter's own embedded completion. (The new async flag is
not strictly needed but it makes the logic easier to reason about)
Finally, harden scmi_xfer_get() to clear async_done when handing out
an xfer, so that no future release-ordering change can leak a stale
arming into a new transaction.
Fixes: 3c3d818a9317 ("firmware: arm_scmi: Add core raw transmission support")
Signed-off-by: Roland Dreier <rolanddreier@rivian.com>
---
drivers/firmware/arm_scmi/driver.c | 1 +
drivers/firmware/arm_scmi/raw_mode.c | 25 +++++++++++++++++++++----
2 files changed, 22 insertions(+), 4 deletions(-)
diff --git a/drivers/firmware/arm_scmi/driver.c b/drivers/firmware/arm_scmi/driver.c
index 847c09da310e..eb20488d39d3 100644
--- a/drivers/firmware/arm_scmi/driver.c
+++ b/drivers/firmware/arm_scmi/driver.c
@@ -718,6 +718,7 @@ static struct scmi_xfer *scmi_xfer_get(const struct scmi_handle *handle,
refcount_set(&xfer->users, 1);
atomic_set(&xfer->busy, SCMI_XFER_FREE);
+ xfer->async_done = NULL;
spin_unlock_irqrestore(&minfo->xfer_lock, flags);
return xfer;
diff --git a/drivers/firmware/arm_scmi/raw_mode.c b/drivers/firmware/arm_scmi/raw_mode.c
index 8751cff5fa4e..5ee21f6b7001 100644
--- a/drivers/firmware/arm_scmi/raw_mode.c
+++ b/drivers/firmware/arm_scmi/raw_mode.c
@@ -198,6 +198,8 @@ struct scmi_raw_mode_info {
* @async_response: A completion to be, optionally, used for async waits: it
* will be setup by @scmi_do_xfer_raw_start, if needed, to be
* pointed at by xfer->async_done.
+ * @async: True if @async_response was armed on @xfer, i.e. if a delayed
+ * response has to be waited for.
* @node: A list node.
*/
struct scmi_xfer_raw_waiter {
@@ -205,6 +207,7 @@ struct scmi_xfer_raw_waiter {
struct scmi_chan_info *cinfo;
struct scmi_xfer *xfer;
struct completion async_response;
+ bool async;
struct list_head node;
};
@@ -349,6 +352,7 @@ scmi_xfer_raw_waiter_get(struct scmi_raw_mode_info *raw, struct scmi_xfer *xfer,
scmi_xfer_async_response_arm(xfer, &rw->async_response);
}
+ rw->async = async;
rw->cinfo = cinfo;
rw->xfer = xfer;
}
@@ -361,8 +365,16 @@ static void scmi_xfer_raw_waiter_put(struct scmi_raw_mode_info *raw,
struct scmi_xfer_raw_waiter *rw)
{
if (rw->xfer) {
+ /*
+ * Disarm the delayed response before this waiter, and its
+ * embedded completion, can be picked up again for a new
+ * transaction: a delayed response received late, after the
+ * related wait timed out, must not signal a completion which
+ * has been in the meantime re-armed on a different xfer.
+ */
scmi_xfer_async_response_disarm(rw->xfer);
rw->xfer = NULL;
+ rw->async = false;
}
mutex_lock(&raw->free_mtx);
@@ -479,18 +491,23 @@ static void scmi_xfer_raw_worker(struct work_struct *work)
ret, scmi_inflight_count(raw->handle));
/* Wait also for an async delayed response if needed */
- if (!ret && xfer->async_done) {
+ if (!ret && rw->async) {
unsigned long tmo = msecs_to_jiffies(SCMI_MAX_RESPONSE_TIMEOUT);
- if (!wait_for_completion_timeout(xfer->async_done, tmo))
+ if (!wait_for_completion_timeout(&rw->async_response, tmo))
dev_err(dev,
"timed out in RAW delayed resp - HDR:%08X\n",
pack_scmi_header(&xfer->hdr));
}
- /* Release waiter and xfer */
- scmi_xfer_raw_put(raw->handle, xfer);
+ /*
+ * Release the waiter first: this disarms the delayed response
+ * while we still hold a reference on the xfer, so that the xfer
+ * cannot be recycled by a new transaction while it still points
+ * at this waiter's completion.
+ */
scmi_xfer_raw_waiter_put(raw, rw);
+ scmi_xfer_raw_put(raw->handle, xfer);
} while (1);
}
--
2.54.0
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH v2 1/2] firmware: arm_scmi: Protect xfer->async_done with xfer->lock
2026-09-09 4:32 ` [PATCH v2 1/2] firmware: arm_scmi: Protect xfer->async_done with xfer->lock Roland Dreier via B4 Relay
@ 2026-09-09 11:18 ` Cristian Marussi
0 siblings, 0 replies; 5+ messages in thread
From: Cristian Marussi @ 2026-09-09 11:18 UTC (permalink / raw)
To: rolanddreier
Cc: Sudeep Holla, Cristian Marussi, arm-scmi, linux-arm-kernel, linux-kernel
On Wed, Sep 09, 2026 at 04:32:27AM +0000, Roland Dreier via B4 Relay wrote:
> From: Roland Dreier <rolanddreier@rivian.com>
>
> Asynchronous SCMI commands are completed by a delayed response. The RX
> path signals the response with complete(xfer->async_done). Unlike
> xfer->done, xfer->async_done is a pointer to a completion owned by
> whoever is waiting for the delayed response, and it stays valid only for
> as long as that waiter is still waiting. In do_xfer_with_response() it
> is a DECLARE_COMPLETION_ONSTACK() in the caller's stack frame.
>
Hi,
LGTM.
Reviewed-by: Cristian Marussi <cristian.marussi@arm.com>
Tested-by: Cristian Marussi <cristian.marussi@arm.com>
Thanks,
Cristian
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH v2 2/2] firmware: arm_scmi: Don't reuse raw xfers with async_done still armed
2026-09-09 4:32 ` [PATCH v2 2/2] firmware: arm_scmi: Don't reuse raw xfers with async_done still armed Roland Dreier via B4 Relay
@ 2026-09-09 11:19 ` Cristian Marussi
0 siblings, 0 replies; 5+ messages in thread
From: Cristian Marussi @ 2026-09-09 11:19 UTC (permalink / raw)
To: rolanddreier
Cc: Sudeep Holla, Cristian Marussi, arm-scmi, linux-arm-kernel, linux-kernel
On Wed, Sep 09, 2026 at 04:32:28AM +0000, Roland Dreier via B4 Relay wrote:
> From: Roland Dreier <rolanddreier@rivian.com>
>
> In SCMI raw mode, scmi_xfer_raw_worker() releases the xfer before
> releasing the waiter that disarms xfer->async_done, and scmi_xfer_get()
> does not clear async_done when it hands out a recycled xfer. A concurrent
> transaction can therefore pick up the xfer while it still points at the
> old waiter's completion, so:
Hi,
LGTM.
Reviewed-by: Cristian Marussi <cristian.marussi@arm.com>
Tested-by: Cristian Marussi <cristian.marussi@arm.com>
Thanks,
Cristian
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-09-09 11:19 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-09 4:32 [PATCH v2 0/2] Couple of SCMI race fixes Roland Dreier via B4 Relay
2026-09-09 4:32 ` [PATCH v2 1/2] firmware: arm_scmi: Protect xfer->async_done with xfer->lock Roland Dreier via B4 Relay
2026-09-09 11:18 ` Cristian Marussi
2026-09-09 4:32 ` [PATCH v2 2/2] firmware: arm_scmi: Don't reuse raw xfers with async_done still armed Roland Dreier via B4 Relay
2026-09-09 11:19 ` Cristian Marussi
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®