Linux RDMA and InfiniBand development
 help / color / mirror / Atom feed
* [PATCH v3 0/2] RDMA/rxe: Fix TOCTOU races on mmap'd send queue
@ 2026-10-07 21:27 Tristan Madani
  2026-10-07 21:27 ` [PATCH v3 1/2] RDMA/rxe: copy send WQE to kernel buffer before processing Tristan Madani
  2026-10-07 21:27 ` [PATCH v3 2/2] RDMA/rxe: copy send WQE to kernel buffer in completer path Tristan Madani
  0 siblings, 2 replies; 5+ messages in thread
From: Tristan Madani @ 2026-10-07 21:27 UTC (permalink / raw)
  To: Zhu Yanjun, Jason Gunthorpe, Leon Romanovsky
  Cc: linux-rdma, linux-kernel, stable, Moni Shoua, Tristan Madani

From: Tristan Madani <tristan@talencesecurity.com>

The rxe driver maps the send queue into userspace via mmap. Both the
requester and completer process Work Queue Entries (WQEs) directly from
this shared buffer without first copying them to kernel memory. This
allows userspace to modify WQE fields concurrently, causing inconsistent
state in the kernel.

This series fixes both paths:
  - Patch 1/2: requester (rxe_req.c) - copy WQE before processing,
    with WRITE_ONCE per-field writeback
  - Patch 2/2: completer (rxe_comp.c) - same treatment, with reuse
    across multi-packet operations to preserve DMA progress

This is the send-path counterpart to the receive-path fixes:
  - commit 22b8fbded65b8 ("RDMA/rxe: Fix TOCTOU heap overflow in
    get_srq_wqe")
  - commit d6ab440240a04 ("RDMA/rxe: Copy WQE to local buffer in
    non-SRQ receive path")

Changes since v2:
  - Replaced bulk memcpy() writeback with targeted WRITE_ONCE() for
    individual fields and smp_store_release() for state transitions,
    avoiding the tearing risk of bulk memcpy on the shared queue
  - Added smp_load_acquire() in the completer to pair with the
    requester's smp_store_release() for state ordering

Changes since v1:
  - Same as v2 changes (v2 only covered patch 2/2)

Tristan Madani (2):
  RDMA/rxe: copy send WQE to kernel buffer before processing
  RDMA/rxe: copy send WQE to kernel buffer in completer path

 drivers/infiniband/sw/rxe/rxe_comp.c  | 58 +++++++++++++++++++++++++--
 drivers/infiniband/sw/rxe/rxe_req.c   | 61 +++++++++++++++++++++++++----
 drivers/infiniband/sw/rxe/rxe_verbs.h | 12 ++++++
 3 files changed, 119 insertions(+), 12 deletions(-)

-- 
2.39.5

^ permalink raw reply	[flat|nested] 5+ messages in thread

* [PATCH v3 1/2] RDMA/rxe: copy send WQE to kernel buffer before processing
  2026-10-07 21:27 [PATCH v3 0/2] RDMA/rxe: Fix TOCTOU races on mmap'd send queue Tristan Madani
@ 2026-10-07 21:27 ` Tristan Madani
  2026-10-07 21:40   ` sashiko-bot
  2026-10-07 21:27 ` [PATCH v3 2/2] RDMA/rxe: copy send WQE to kernel buffer in completer path Tristan Madani
  1 sibling, 1 reply; 5+ messages in thread
From: Tristan Madani @ 2026-10-07 21:27 UTC (permalink / raw)
  To: Zhu Yanjun, Jason Gunthorpe, Leon Romanovsky
  Cc: linux-rdma, linux-kernel, stable, Moni Shoua, Tristan Madani

From: Tristan Madani <tristan@talencesecurity.com>

The rxe send queue is mapped into userspace via mmap. The requester
processes Work Queue Entries (WQEs) directly from this shared buffer
without first copying them to kernel memory. Userspace can modify WQE
fields (num_sge, sge_offset, SGE entries) between kernel reads,
leading to inconsistent state in copy_data().

This is the send-path counterpart to the receive-path fixes:
  - commit 22b8fbded65b8 ("RDMA/rxe: Fix TOCTOU heap overflow in
    get_srq_wqe")
  - commit d6ab440240a04 ("RDMA/rxe: Copy WQE to local buffer in
    non-SRQ receive path")

Fix by copying the send WQE to a kernel-private buffer in req_next_wqe()
before processing. The local copy is reused across multi-packet sends
to preserve DMA progress state (cur_sge, sge_offset, resid). Field
updates are written back to the shared queue using WRITE_ONCE() for
individual fields and smp_store_release() for state transitions, so
the completer and userspace observe consistent values without the
tearing risk of bulk memcpy().

The copy is invalidated when the WQE index advances (last packet sent,
error, local ops, UD oversized) or when a retry resets WQE state.

Fixes: 8700e3e7c485 ("Soft RoCE driver")
Cc: stable@vger.kernel.org
Signed-off-by: Tristan Madani <tristan@talencesecurity.com>
---
 drivers/infiniband/sw/rxe/rxe_req.c   | 59 +++++++++++++++++++++++++--
 drivers/infiniband/sw/rxe/rxe_verbs.h |  6 +++
 2 files changed, 61 insertions(+), 4 deletions(-)

diff --git a/drivers/infiniband/sw/rxe/rxe_req.c b/drivers/infiniband/sw/rxe/rxe_req.c
index 24f5c044363f7..c72de69630e58 100644
--- a/drivers/infiniband/sw/rxe/rxe_req.c
+++ b/drivers/infiniband/sw/rxe/rxe_req.c
@@ -161,6 +161,26 @@ static void req_check_sq_drain_done(struct rxe_qp *qp)
 	spin_unlock_irqrestore(&qp->state_lock, flags);
 }
 
+/* Write back requester WQE fields to shared memory using targeted
+ * stores so the completer and userspace observe consistent state.
+ */
+static void rxe_req_writeback_wqe(struct rxe_qp *qp)
+{
+	struct rxe_send_wqe *shared = qp->req.shared_wqe;
+	struct rxe_send_wqe *local = &qp->req.send_wqe.wqe;
+
+	if (!qp->req.send_wqe_valid || !shared)
+		return;
+
+	WRITE_ONCE(shared->status, local->status);
+	WRITE_ONCE(shared->first_psn, local->first_psn);
+	WRITE_ONCE(shared->last_psn, local->last_psn);
+	WRITE_ONCE(shared->mask, local->mask);
+	WRITE_ONCE(shared->has_rd_atomic, local->has_rd_atomic);
+	/* State must be last so the completer sees prior updates */
+	smp_store_release(&shared->state, local->state);
+}
+
 static struct rxe_send_wqe *__req_next_wqe(struct rxe_qp *qp)
 {
 	struct rxe_queue *q = qp->sq.queue;
@@ -178,6 +198,8 @@ static struct rxe_send_wqe *req_next_wqe(struct rxe_qp *qp)
 {
 	struct rxe_send_wqe *wqe;
 	unsigned long flags;
+	unsigned int num_sge;
+	size_t copy_size;
 
 	req_check_sq_drain_done(qp);
 
@@ -193,6 +215,24 @@ static struct rxe_send_wqe *req_next_wqe(struct rxe_qp *qp)
 	}
 	spin_unlock_irqrestore(&qp->state_lock, flags);
 
+	/* Reuse the existing kernel-private copy if still valid */
+	if (qp->req.send_wqe_valid && qp->req.shared_wqe == wqe)
+		return &qp->req.send_wqe.wqe;
+
+	/* Copy WQE from userspace-mapped shared queue to kernel-private
+	 * buffer to prevent TOCTOU races on DMA state fields.
+	 */
+	num_sge = wqe->dma.num_sge;
+	if (unlikely(num_sge > qp->sq.max_sge)) {
+		rxe_dbg_qp(qp, "invalid num_sge in send WQE\n");
+		return NULL;
+	}
+	copy_size = sizeof(*wqe) + num_sge * sizeof(struct rxe_sge);
+	memcpy(&qp->req.send_wqe.wqe, wqe, copy_size);
+	qp->req.shared_wqe = wqe;
+	qp->req.send_wqe_valid = true;
+
+	wqe = &qp->req.send_wqe.wqe;
 	wqe->mask = wr_opcode_mask(wqe->wr.opcode, qp);
 	return wqe;
 }
@@ -582,9 +622,13 @@ static void update_state(struct rxe_qp *qp, struct rxe_pkt_info *pkt)
 {
 	qp->req.opcode = pkt->opcode;
 
-	if (pkt->mask & RXE_END_MASK)
+	rxe_req_writeback_wqe(qp);
+
+	if (pkt->mask & RXE_END_MASK) {
 		qp->req.wqe_index = queue_next_index(qp->sq.queue,
 						     qp->req.wqe_index);
+		qp->req.send_wqe_valid = false;
+	}
 
 	qp->need_req_skb = 0;
 
@@ -634,7 +678,9 @@ static int rxe_do_local_ops(struct rxe_qp *qp, struct rxe_send_wqe *wqe)
 
 	wqe->state = wqe_state_done;
 	wqe->status = IB_WC_SUCCESS;
+	rxe_req_writeback_wqe(qp);
 	qp->req.wqe_index = queue_next_index(qp->sq.queue, qp->req.wqe_index);
+	qp->req.send_wqe_valid = false;
 
 	return 0;
 }
@@ -695,6 +741,7 @@ int rxe_requester(struct rxe_qp *qp)
 	if (unlikely(qp->req.need_retry && !qp->req.wait_for_rnr_timer)) {
 		req_retry(qp);
 		qp->req.need_retry = 0;
+		qp->req.send_wqe_valid = false;
 	}
 
 	wqe = req_next_wqe(qp);
@@ -772,10 +819,12 @@ int rxe_requester(struct rxe_qp *qp)
 			wqe->last_psn = qp->req.psn;
 			qp->req.psn = (qp->req.psn + 1) & BTH_PSN_MASK;
 			qp->req.opcode = IB_OPCODE_UD_SEND_ONLY;
-			qp->req.wqe_index = queue_next_index(qp->sq.queue,
-						       qp->req.wqe_index);
 			wqe->state = wqe_state_done;
 			wqe->status = IB_WC_SUCCESS;
+			rxe_req_writeback_wqe(qp);
+			qp->req.wqe_index = queue_next_index(qp->sq.queue,
+						       qp->req.wqe_index);
+			qp->req.send_wqe_valid = false;
 			goto done;
 		}
 		payload = mtu;
@@ -839,8 +888,10 @@ int rxe_requester(struct rxe_qp *qp)
 	goto out;
 err:
 	/* update wqe_index for each wqe completion */
-	qp->req.wqe_index = queue_next_index(qp->sq.queue, qp->req.wqe_index);
 	wqe->state = wqe_state_error;
+	rxe_req_writeback_wqe(qp);
+	qp->req.wqe_index = queue_next_index(qp->sq.queue, qp->req.wqe_index);
+	qp->req.send_wqe_valid = false;
 	rxe_qp_error(qp);
 exit:
 	ret = -EAGAIN;
diff --git a/drivers/infiniband/sw/rxe/rxe_verbs.h b/drivers/infiniband/sw/rxe/rxe_verbs.h
index 0f5ffd94643f9..a22dfc6e5ae3c 100644
--- a/drivers/infiniband/sw/rxe/rxe_verbs.h
+++ b/drivers/infiniband/sw/rxe/rxe_verbs.h
@@ -114,6 +114,12 @@ struct rxe_req_info {
 	int			wait_for_rnr_timer;
 	int			noack_pkts;
 	int			again;
+	struct rxe_send_wqe	*shared_wqe;
+	bool			send_wqe_valid;
+	struct {
+		struct rxe_send_wqe	wqe;
+		struct ib_sge		sge[RXE_MAX_SGE];
+	} send_wqe;
 };
 
 struct rxe_comp_info {
-- 
2.47.3


^ permalink raw reply related	[flat|nested] 5+ messages in thread

* [PATCH v3 2/2] RDMA/rxe: copy send WQE to kernel buffer in completer path
  2026-10-07 21:27 [PATCH v3 0/2] RDMA/rxe: Fix TOCTOU races on mmap'd send queue Tristan Madani
  2026-10-07 21:27 ` [PATCH v3 1/2] RDMA/rxe: copy send WQE to kernel buffer before processing Tristan Madani
@ 2026-10-07 21:27 ` Tristan Madani
  2026-10-07 21:49   ` sashiko-bot
  1 sibling, 1 reply; 5+ messages in thread
From: Tristan Madani @ 2026-10-07 21:27 UTC (permalink / raw)
  To: Zhu Yanjun, Jason Gunthorpe, Leon Romanovsky
  Cc: linux-rdma, linux-kernel, stable, Moni Shoua, Tristan Madani

From: Tristan Madani <tristan@talencesecurity.com>

The rxe completer processes send Work Queue Entries (WQEs) directly
from the mmap'd shared send queue without copying them to kernel memory.
Userspace can modify WQE fields (num_sge, opcode, PSN values, DMA state)
while the completer is processing them, leading to inconsistent
decisions in check_psn() and check_ack(), and potential out-of-bounds
access in copy_data() via a corrupted dma.num_sge.

This is the completer-path counterpart to the previous commit which
fixed the requester path.

Fix by copying the WQE to a kernel-private buffer in get_wqe() before
processing. The local copy is reused across multi-packet operations
(e.g. RDMA READ responses) when the WQE state is unchanged, preserving
DMA progress across completer invocations. The copy is refreshed when
the state changes (via smp_load_acquire pairing with the requester
smp_store_release) to ensure PSN and other fields are consistent.

Field updates (status, has_rd_atomic) are written back to the shared
queue using WRITE_ONCE() before advancing the consumer pointer, so
userspace observes consistent completion values.

Fixes: 8700e3e7c485 ("Soft RoCE driver")
Cc: stable@vger.kernel.org
Signed-off-by: Tristan Madani <tristan@talencesecurity.com>
---
 drivers/infiniband/sw/rxe/rxe_comp.c  | 54 ++++++++++++++++++++++++++-
 drivers/infiniband/sw/rxe/rxe_verbs.h |  6 +++
 2 files changed, 58 insertions(+), 2 deletions(-)

diff --git a/drivers/infiniband/sw/rxe/rxe_comp.c b/drivers/infiniband/sw/rxe/rxe_comp.c
index 1390e861bd1d7..f57c4396c28cb 100644
--- a/drivers/infiniband/sw/rxe/rxe_comp.c
+++ b/drivers/infiniband/sw/rxe/rxe_comp.c
@@ -88,6 +88,18 @@ static inline unsigned long rnrnak_jiffies(u8 timeout)
 		usecs_to_jiffies(rnrnak_usec[timeout]), 1);
 }
 
+static void rxe_comp_writeback_wqe(struct rxe_qp *qp)
+{
+	struct rxe_send_wqe *shared = qp->comp.shared_wqe;
+	struct rxe_send_wqe *local = &qp->comp.comp_wqe.wqe;
+
+	if (!qp->comp.comp_wqe_valid || !shared)
+		return;
+
+	WRITE_ONCE(shared->status, local->status);
+	WRITE_ONCE(shared->has_rd_atomic, local->has_rd_atomic);
+}
+
 static enum ib_wc_opcode wr_to_wc_opcode(enum ib_wr_opcode opcode)
 {
 	switch (opcode) {
@@ -142,17 +154,52 @@ static inline enum comp_state get_wqe(struct rxe_qp *qp,
 				      struct rxe_send_wqe **wqe_p)
 {
 	struct rxe_send_wqe *wqe;
+	u32 state;
+	unsigned int num_sge;
 
 	/* we come here whether or not we found a response packet to see if
 	 * there are any posted WQEs
 	 */
 	wqe = queue_head(qp->sq.queue, QUEUE_TYPE_FROM_CLIENT);
-	*wqe_p = wqe;
 
 	/* no WQE or requester has not started it yet */
-	if (!wqe || wqe->state == wqe_state_posted)
+	if (!wqe) {
+		*wqe_p = NULL;
 		return pkt ? COMPST_DONE : COMPST_EXIT;
+	}
 
+	/* Pairs with smp_store_release() in rxe_req_writeback_wqe() */
+	state = smp_load_acquire(&wqe->state);
+	if (state == wqe_state_posted) {
+		*wqe_p = wqe;
+		return pkt ? COMPST_DONE : COMPST_EXIT;
+	}
+
+	/* Reuse existing local copy if still processing same WQE
+	 * with unchanged state (preserves DMA progress for multi-packet ops)
+	 */
+	if (qp->comp.comp_wqe_valid && qp->comp.shared_wqe == wqe &&
+	    qp->comp.comp_wqe.wqe.state == state) {
+		wqe = &qp->comp.comp_wqe.wqe;
+		*wqe_p = wqe;
+		goto check_state;
+	}
+
+	/* Copy shared WQE to kernel-private buffer */
+	num_sge = wqe->dma.num_sge;
+	if (unlikely(num_sge > RXE_MAX_SGE))
+		num_sge = RXE_MAX_SGE;
+
+	qp->comp.shared_wqe = wqe;
+	memcpy(&qp->comp.comp_wqe.wqe, wqe,
+	       sizeof(*wqe) + num_sge * sizeof(struct rxe_sge));
+	qp->comp.comp_wqe_valid = true;
+	qp->comp.comp_wqe.wqe.dma.num_sge = num_sge;
+
+	wqe = &qp->comp.comp_wqe.wqe;
+	*wqe_p = wqe;
+
+check_state:
 	/* WQE does not require an ack */
 	if (wqe->state == wqe_state_done)
 		return COMPST_COMP_WQE;
@@ -454,6 +501,9 @@ static void do_complete(struct rxe_qp *qp, struct rxe_send_wqe *wqe)
 	if (post)
 		make_send_cqe(qp, wqe, &cqe);
 
+	rxe_comp_writeback_wqe(qp);
+	qp->comp.comp_wqe_valid = false;
+
 	queue_advance_consumer(qp->sq.queue, QUEUE_TYPE_FROM_CLIENT);
 
 	if (post)
diff --git a/drivers/infiniband/sw/rxe/rxe_verbs.h b/drivers/infiniband/sw/rxe/rxe_verbs.h
index a22dfc6e5ae3c..6205dbc29dff6 100644
--- a/drivers/infiniband/sw/rxe/rxe_verbs.h
+++ b/drivers/infiniband/sw/rxe/rxe_verbs.h
@@ -130,6 +130,12 @@ struct rxe_comp_info {
 	int			started_retry;
 	u32			retry_cnt;
 	u32			rnr_retry;
+	struct rxe_send_wqe	*shared_wqe;
+	bool			comp_wqe_valid;
+	struct {
+		struct rxe_send_wqe	wqe;
+		struct ib_sge		sge[RXE_MAX_SGE];
+	} comp_wqe;
 };
 
 /* responder states */
-- 
2.47.3


^ permalink raw reply related	[flat|nested] 5+ messages in thread

* Re: [PATCH v3 1/2] RDMA/rxe: copy send WQE to kernel buffer before processing
  2026-10-07 21:27 ` [PATCH v3 1/2] RDMA/rxe: copy send WQE to kernel buffer before processing Tristan Madani
@ 2026-10-07 21:40   ` sashiko-bot
  0 siblings, 0 replies; 5+ messages in thread
From: sashiko-bot @ 2026-10-07 21:40 UTC (permalink / raw)
  To: Tristan Madani; +Cc: linux-rdma

Thank you for your contribution! Sashiko AI review found 8 potential issue(s) to consider:
- [High] rxe: Silent QP stall on invalid WQE num_sge
- [High] rxe: Stale WQE cache reuse across QP resets
- [High] rxe: Data clobbering of flush error state in rxe_requester
- [High] rxe: Out-of-bounds array read via unvalidated WQE opcode
- [High] rxe: TOCTOU info leak during WQE copy due to compiler reload
- [High] rxe: Uninitialized kernel memory transmission on inline sends
- [High] rxe: RDMA READ retries broken due to missing DMA state writeback
- [Medium] rxe: FORTIFY_SOURCE field-spanning write warning in req_next_wqe memcpy
--

commit 99abb995a563e5638c46b6895ade6985823dd689
Author: Tristan Madani <tristan@talencesecurity.com>

    RDMA/rxe: copy send WQE to kernel buffer before processing

This commit introduces a local kernel-private copy of the requester Work Queue
Entries (WQEs) to prevent TOCTOU vulnerabilities during send queue processing.
It copies the WQE fields into a local buffer before validation and uses
targeted writes to update the shared completion state. A validity flag tracks
when the cached WQE is active for reuse across multi-packet sends.

> diff --git a/drivers/infiniband/sw/rxe/rxe_req.c b/drivers/infiniband/sw/rxe/rxe_req.c
> index 53f7c1e7fe55b..025a41145c408 100644
> --- a/drivers/infiniband/sw/rxe/rxe_req.c
> +++ b/drivers/infiniband/sw/rxe/rxe_req.c
> @@ -161,6 +161,26 @@ static void req_check_sq_drain_done(struct rxe_qp *qp)
>  	spin_unlock_irqrestore(&qp->state_lock, flags);
>  }
>  
> +/* Write back requester WQE fields to shared memory using targeted
> + * stores so the completer and userspace observe consistent state.
> + */
> +static void rxe_req_writeback_wqe(struct rxe_qp *qp)
> +{
> +	struct rxe_send_wqe *shared = qp->req.shared_wqe;
> +	struct rxe_send_wqe *local = &qp->req.send_wqe.wqe;
> +
> +	if (!qp->req.send_wqe_valid || !shared)
> +		return;
> +
> +	WRITE_ONCE(shared->status, local->status);
> +	WRITE_ONCE(shared->first_psn, local->first_psn);
> +	WRITE_ONCE(shared->last_psn, local->last_psn);
> +	WRITE_ONCE(shared->mask, local->mask);
> +	WRITE_ONCE(shared->has_rd_atomic, local->has_rd_atomic);
> +	/* State must be last so the completer sees prior updates */
> +	smp_store_release(&shared->state, local->state);
> +}

[Severity: High]
Does omitting DMA state updates here break RDMA READ retries?

To protect against TOCTOU attacks, this writeback deliberately omits
DMA state updates like resid and cur_sge.

However, the untouched req_retry() function reads wqe->dma.resid directly
from the shared WQE to calculate the retry PSN offset:

    npsn = (wqe->dma.length - wqe->dma.resid) / qp->mtu;

Because the shared resid is never updated by the processing path anymore,
req_retry() incorrectly computes that no progress was made. This sends
retries from the beginning of the buffer and breaks RDMA READ response state.

[ ... ]

> @@ -193,6 +215,24 @@ static struct rxe_send_wqe *req_next_wqe(struct rxe_qp *qp)
>  	}
>  	spin_unlock_irqrestore(&qp->state_lock, flags);
>  
> +	/* Reuse the existing kernel-private copy if still valid */
> +	if (qp->req.send_wqe_valid && qp->req.shared_wqe == wqe)
> +		return &qp->req.send_wqe.wqe;

[Severity: High]
Can this erroneously reuse a stale cached WQE across QP resets?

When a QP transitions to IB_QPS_RESET, rxe_qp_reset() resets the queue
indices to 0 but does not clear qp->req.send_wqe_valid.

If the QP is later transitioned back to RTS and userspace posts a new WQE at
index 0, req_next_wqe() evaluates this condition. Since the address matches
the pre-reset WQE at index 0, it incorrectly reuses the stale cached WQE
instead of copying and processing the newly posted WQE.

> +
> +	/* Copy WQE from userspace-mapped shared queue to kernel-private
> +	 * buffer to prevent TOCTOU races on DMA state fields.
> +	 */
> +	num_sge = wqe->dma.num_sge;
> +	if (unlikely(num_sge > qp->sq.max_sge)) {
> +		rxe_dbg_qp(qp, "invalid num_sge in send WQE\n");
> +		return NULL;
> +	}

[Severity: High]
Does returning NULL here stall the queue?

If this validation fails, it returns NULL. The caller rxe_requester() handles
a NULL return by exiting the task loop and returning -EAGAIN. Because the WQE
is not explicitly failed with an error status, the queue consumer index is
never advanced and the QP is not transitioned to IB_QPS_ERR. The QP will
silently hang forever, repeatedly trying to process this malformed WQE.

> +	copy_size = sizeof(*wqe) + num_sge * sizeof(struct rxe_sge);
> +	memcpy(&qp->req.send_wqe.wqe, wqe, copy_size);

[Severity: Medium]
Will this memcpy trigger a FORTIFY_SOURCE warning or panic?

The destination pointer &qp->req.send_wqe.wqe is strongly typed to the wqe
member of the wrapper struct, so the compiler evaluates __builtin_object_size
as exactly sizeof(struct rxe_send_wqe). When num_sge > 0, copy_size exceeds
this struct size, which can trigger a __fortify_memcpy_chk warning for a
field-spanning write into the adjacent sge array.

[Severity: High]
Is there a TOCTOU info leak here due to a compiler reload during memcpy?

The variable num_sge is read from shared memory, validated against
qp->sq.max_sge, and used to compute copy_size.

If userspace maliciously inflates wqe->dma.num_sge in shared memory after
the local variable is read but before memcpy executes, the copied struct
header will contain the inflated num_sge value.

Because memcpy only copies bytes up to the original copy_size, the trailing
SGE array elements in the local buffer remain uninitialized. The secondary
validation in rxe_requester() reads the modified num_sge from the local copy,
validates it against max_sge, and processes the uninitialized SGE elements,
potentially leaking kernel memory to the network.

[Severity: High]
Will this transmit uninitialized kernel memory on inline sends?

For inline sends, the payload resides in the inline_data flex array following
the WQE header, often with num_sge == 0.

If num_sge is 0, the copy_size calculation results in strictly copying the WQE
header, entirely omitting the inline data payload. When the requester later
processes the packet, it reads the payload from the local copy's inline_data
union array, transmitting uninitialized kernel memory instead of the user's
data.

> +	qp->req.shared_wqe = wqe;
> +	qp->req.send_wqe_valid = true;
> +
> +	wqe = &qp->req.send_wqe.wqe;
>  	wqe->mask = wr_opcode_mask(wqe->wr.opcode, qp);

[Severity: High]
Can this result in an out-of-bounds array read?

At this point, wqe->wr.opcode has just been copied from userspace and is
entirely unvalidated. wr_opcode_mask() uses this opcode directly as an
array index into rxe_wr_opcode_info, which can lead to an out-of-bounds
kernel memory read and a system crash.

>  	return wqe;
>  }

[ ... ]

> @@ -839,8 +888,10 @@ int rxe_requester(struct rxe_qp *qp)
>  	goto out;
>  err:
>  	/* update wqe_index for each wqe completion */
> -	qp->req.wqe_index = queue_next_index(qp->sq.queue, qp->req.wqe_index);
>  	wqe->state = wqe_state_error;
> +	rxe_req_writeback_wqe(qp);

[Severity: High]
Does this writeback clobber the flush error state with stale cached data?

If a QP enters IB_QPS_ERR while a WQE is locally cached, rxe_requester()
fetches the shared WQE directly via __req_next_wqe(). It assigns
wqe->status = IB_WC_WR_FLUSH_ERR and jumps to err:, setting
wqe->state = wqe_state_error directly on the shared WQE.

Then it calls rxe_req_writeback_wqe(qp). Since qp->req.send_wqe_valid is
still true, the writeback function writes the stale local->status and
local->state over the shared WQE, erasing the flush error state and
potentially stalling completions.

> +	qp->req.wqe_index = queue_next_index(qp->sq.queue, qp->req.wqe_index);
> +	qp->req.send_wqe_valid = false;
>  	rxe_qp_error(qp);
>  exit:
>  	ret = -EAGAIN;

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261007212737.1989004-1-tristmd@gmail.com?part=1

^ permalink raw reply	[flat|nested] 5+ messages in thread

* Re: [PATCH v3 2/2] RDMA/rxe: copy send WQE to kernel buffer in completer path
  2026-10-07 21:27 ` [PATCH v3 2/2] RDMA/rxe: copy send WQE to kernel buffer in completer path Tristan Madani
@ 2026-10-07 21:49   ` sashiko-bot
  0 siblings, 0 replies; 5+ messages in thread
From: sashiko-bot @ 2026-10-07 21:49 UTC (permalink / raw)
  To: Tristan Madani; +Cc: linux-rdma

Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [Critical] rdma/rxe: TOCTOU mitigation bypass allows overwriting kernel DMA progress
- [High] rdma/rxe: out-of-bounds read in get_wqe() due to incorrect num_sge bounding
- [High] rdma/rxe: stale WQE reuse due to uncleared comp_wqe_valid on QP reset
- [High] rdma/rxe: TOCTOU double-fetch of num_sge due to missing READ_ONCE()
- [High] rdma/rxe: torn read of WQE state and status during memcpy causes successful completion of failed WQE
--

commit 925c6c168a2ebbc13c604cb14fd0fda0918233a7
Author: Tristan Madani <tristan@talencesecurity.com>

    RDMA/rxe: copy send WQE to kernel buffer in completer path

The commit modifies the rxe driver's completer path to copy Work Queue
Entries (WQEs) to a kernel-private buffer to prevent TOCTOU modifications
from userspace. The copied WQE is reused across multi-packet operations
when the state is unchanged to preserve DMA progress. Field updates are
written back to the shared queue to ensure consistent completion values.

> diff --git a/drivers/infiniband/sw/rxe/rxe_comp.c b/drivers/infiniband/sw/rxe/rxe_comp.c
> index 1390e861bd1d7..f57c4396c28cb 100644
> --- a/drivers/infiniband/sw/rxe/rxe_comp.c
> +++ b/drivers/infiniband/sw/rxe/rxe_comp.c
[ ... ]
> @@ -142,17 +154,52 @@ static inline enum comp_state get_wqe(struct rxe_qp *qp,
>  				      struct rxe_send_wqe **wqe_p)
>  {
>  	struct rxe_send_wqe *wqe;
> +	u32 state;
> +	unsigned int num_sge;
>  
>  	/* we come here whether or not we found a response packet to see if
>  	 * there are any posted WQEs
>  	 */
>  	wqe = queue_head(qp->sq.queue, QUEUE_TYPE_FROM_CLIENT);
> -	*wqe_p = wqe;
>  
>  	/* no WQE or requester has not started it yet */
> -	if (!wqe || wqe->state == wqe_state_posted)
> +	if (!wqe) {
> +		*wqe_p = NULL;
>  		return pkt ? COMPST_DONE : COMPST_EXIT;
> +	}
>  
> +	/* Pairs with smp_store_release() in rxe_req_writeback_wqe() */
> +	state = smp_load_acquire(&wqe->state);
> +	if (state == wqe_state_posted) {
> +		*wqe_p = wqe;
> +		return pkt ? COMPST_DONE : COMPST_EXIT;
> +	}
> +
> +	/* Reuse existing local copy if still processing same WQE
> +	 * with unchanged state (preserves DMA progress for multi-packet ops)
> +	 */
> +	if (qp->comp.comp_wqe_valid && qp->comp.shared_wqe == wqe &&
> +	    qp->comp.comp_wqe.wqe.state == state) {

[Severity: Critical]
If a malicious userspace application alters wqe->state in the shared ring
buffer mid-operation to a different but non-terminating value, will this
equality check fail?

If it fails, the completer will fall through and recopy the entire WQE from
shared memory into the kernel buffer qp->comp.comp_wqe.

Will this recopy overwrite the kernel's valid DMA progress (resid,
sge_offset, cur_sge) with the newly crafted dma state and lengths from
userspace, defeating the protection?

[Severity: High]
When a Queue Pair (QP) is transitioned to the RESET state via ib_modify_qp(),
rxe_qp_reset() is called. Does rxe_qp_reset() clear the newly introduced
qp->comp.comp_wqe_valid and qp->req.send_wqe_valid flags?

If these flags are not cleared, and userspace posts a new WQE after restart,
it is placed at queue index 0. The memory address wqe would evaluate to the
exact same pointer as the pre-reset operation.

Would this cause the completer to unexpectedly reuse the stale kernel-private
WQE, ignoring the newly posted operation's parameters?

> +		wqe = &qp->comp.comp_wqe.wqe;
> +		*wqe_p = wqe;
> +		goto check_state;
> +	}
> +
> +	/* Copy shared WQE to kernel-private buffer */
> +	num_sge = wqe->dma.num_sge;
> +	if (unlikely(num_sge > RXE_MAX_SGE))
> +		num_sge = RXE_MAX_SGE;
> +
> +	qp->comp.shared_wqe = wqe;
> +	memcpy(&qp->comp.comp_wqe.wqe, wqe,
> +	       sizeof(*wqe) + num_sge * sizeof(struct rxe_sge));

[Severity: High]
Since wqe->dma.num_sge is read from an mmap'd shared memory queue into num_sge
without using READ_ONCE(), is the C compiler permitted to optimize the code
by reloading the value from memory during the memcpy size calculation?

If an attacker in userspace concurrently modifies wqe->dma.num_sge to a large
value after the bounds check, could the re-fetched value bypass the
RXE_MAX_SGE limit and cause memcpy to write out of bounds into the fixed-size
qp->comp.comp_wqe kernel buffer?

[Severity: High]
The code caps num_sge against the global maximum RXE_MAX_SGE (32) instead
of the queue's specific maximum qp->sq.max_sge.

If max_sge was configured to a small value (e.g., 1), the allocated queue
elements are much smaller. If a malicious element is near the end of the
vmalloc_user ring buffer, could the memcpy read past the allocated queue
memory?

Could this hit an unmapped guard page and cause a fatal kernel page fault,
or leak adjacent queue elements?

[Severity: High]
Because memcpy uses unordered loads to copy the WQE from shared memory, could it
experience a torn read where it captures a new wqe->state (wqe_state_error)
but an old wqe->status (IB_WC_SUCCESS)?

> +	qp->comp.comp_wqe_valid = true;
> +	qp->comp.comp_wqe.wqe.dma.num_sge = num_sge;
> +
> +	wqe = &qp->comp.comp_wqe.wqe;
> +	*wqe_p = wqe;
> +
> +check_state:
>  	/* WQE does not require an ack */
>  	if (wqe->state == wqe_state_done)
>  		return COMPST_COMP_WQE;

If get_wqe() later checks the local copy's state directly:

check_state:
	/* WQE caused an error */
	if (wqe->state == wqe_state_error)
		return COMPST_ERROR;

Will the state machine transition to COMPST_ERROR but process the completion
as IB_WC_SUCCESS because the torn read missed the status update?

Does the local variable wqe need to be updated with the cleanly loaded state
variable from earlier, or should the status and state be read more carefully?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261007212737.1989004-1-tristmd@gmail.com?part=2

^ permalink raw reply	[flat|nested] 5+ messages in thread

end of thread, other threads:[~2026-10-07 21:49 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-07 21:27 [PATCH v3 0/2] RDMA/rxe: Fix TOCTOU races on mmap'd send queue Tristan Madani
2026-10-07 21:27 ` [PATCH v3 1/2] RDMA/rxe: copy send WQE to kernel buffer before processing Tristan Madani
2026-10-07 21:40   ` sashiko-bot
2026-10-07 21:27 ` [PATCH v3 2/2] RDMA/rxe: copy send WQE to kernel buffer in completer path Tristan Madani
2026-10-07 21:49   ` sashiko-bot

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox