From: Yishai Hadas <yishaih@nvidia.com>
To: <jgg@ziepe.ca>, <leon@kernel.org>
Cc: <linux-rdma@vger.kernel.org>, <netdev@vger.kernel.org>,
<saeedm@nvidia.com>, <tariqt@nvidia.com>, <mbloch@nvidia.com>,
<kuba@kernel.org>, <pabeni@redhat.com>, <edumazet@kernel.org>,
<davem@davemloft.net>, <andrew+netdev@lunn.ch>,
<dtatulea@nvidia.com>, <moshe@nvidia.com>, <yishaih@nvidia.com>,
<maorg@nvidia.com>
Subject: [PATCH V1 rdma-next 13/24] RDMA/mlx5: Verify UMR CQ polling against a hostile NIC
Date: Tue, 10 Nov 2026 15:43:09 +0200 [thread overview]
Message-ID: <20261110134320.34408-14-yishaih@nvidia.com> (raw)
In-Reply-To: <20261110134320.34408-1-yishaih@nvidia.com>
The generic poll path trusts the CQE content: its opcode, QPN and
wqe_counter decide which request is completed and which waiter is woken.
A forged CQE can therefore complete a request the driver never posted,
or one that already completed. The driver then writes through a context
it no longer owns, which corrupts memory, as the context is usually on
the waiter's stack.
In adversarial mode, install a dedicated poll_one for the UMR CQ that
does not take the CQE's word for it. The driver keeps a shadow FIFO of
its posted requests, and a CQE is accepted only if its opcode and QPN
match the UMR QP and its full wqe_counter matches the oldest pending
request, one already passed to the doorbell. The context is then taken
from the FIFO, never from the CQE. This cannot prove that the NIC
really executed the request, as it can always report the oldest one as
completed; it ensures that the driver completes only a context it owns,
once and in order.
A CQE that fails validation proves the NIC is forging completions, so
nothing it reports on this QP can be trusted any more. Mark the UMR
path permanently failed, so neither a new post nor a QP recovery can
reopen it, stop interpreting this CQ and force the mlx5_core health
recovery. The forced variant is needed because such a NIC still looks
healthy to the fatal sensors. Recovery moves the device to the internal
error state, and the existing flush then completes the pending requests
in software and wakes their waiters, so this path does not complete
them itself.
A non-flush error CQE is valid only while the UMR path is active: a real
failure moves the QP to error, after which the NIC returns only flush
errors until the recovery resets it. Accept one only by moving the state
from active to recover atomically, and treat it as forged otherwise, so
a single recovery runs at a time.
On teardown, stop polling the CQ before destroying the UMR QP, so a CQE
for the destroyed QP, or for a reused QPN, is never completed to a
waiter.
Signed-off-by: Yishai Hadas <yishaih@nvidia.com>
---
drivers/infiniband/hw/mlx5/cq.c | 165 +++++++++++++++++++++++++++
drivers/infiniband/hw/mlx5/mlx5_ib.h | 5 +
drivers/infiniband/hw/mlx5/umr.c | 38 ++++++
3 files changed, 208 insertions(+)
diff --git a/drivers/infiniband/hw/mlx5/cq.c b/drivers/infiniband/hw/mlx5/cq.c
index 11c1153747c2..c6c3d02709aa 100644
--- a/drivers/infiniband/hw/mlx5/cq.c
+++ b/drivers/infiniband/hw/mlx5/cq.c
@@ -37,6 +37,7 @@
#include "mlx5_ib.h"
#include "srq.h"
#include "qp.h"
+#include "umr.h"
#define UVERBS_MODULE_NAME mlx5_ib
#include <rdma/uverbs_named_ioctl.h>
@@ -608,6 +609,170 @@ static int poll_soft_wc(struct mlx5_ib_cq *cq, int num_entries,
return npolled;
}
+static int mlx5_ib_poll_one_stopped(struct mlx5_ib_cq *cq,
+ struct mlx5_ib_qp **cur_qp, struct ib_wc *wc)
+{
+ return -EAGAIN;
+}
+
+/**
+ * mlx5_ib_cq_stop_polling - stop interpreting CQEs on a CQ
+ * @ibcq: the CQ
+ *
+ * Takes cq->lock, which mlx5_ib_poll_cq() holds while polling, so no poll
+ * is in flight on return and later polls return no CQE from the NIC.
+ */
+void mlx5_ib_cq_stop_polling(struct ib_cq *ibcq)
+{
+ struct mlx5_ib_cq *cq = to_mcq(ibcq);
+ unsigned long flags;
+
+ spin_lock_irqsave(&cq->lock, flags);
+ cq->poll_one = mlx5_ib_poll_one_stopped;
+ spin_unlock_irqrestore(&cq->lock, flags);
+}
+
+/*
+ * poll_one of the UMR CQ in adversarial_mode. A CQE is accepted only if it
+ * matches the oldest request in the shadow FIFO, otherwise the device is
+ * treated as compromised.
+ */
+int mlx5_ib_poll_one_umr(struct mlx5_ib_cq *cq, struct mlx5_ib_qp **cur_qp,
+ struct ib_wc *wc)
+{
+ struct mlx5_ib_dev *dev = to_mdev(cq->ibcq.device);
+ struct mlx5r_umr_context *expected;
+ struct umr_common *umrc = &dev->umrc;
+ struct mlx5_err_cqe err_cqe;
+ struct mlx5_cqe64 *cqe64;
+ struct mlx5_ib_wq *wq;
+ struct mlx5_ib_qp *qp;
+ struct ib_qp *ibqp;
+ u16 wqe_ctr;
+ u8 opcode;
+ u32 head;
+ u32 sop_qpn;
+ u32 qpn;
+ u8 sop;
+ int idx;
+ void *cqe;
+
+ cqe = next_cqe_sw(cq);
+ if (!cqe)
+ return -EAGAIN;
+
+ cqe64 = (cq->mcq.cqe_sz == 64) ? cqe : cqe + 64;
+ ++cq->mcq.cons_index;
+ /*
+ * Read each field once, so a NIC rewriting the CQE cannot change it
+ * between validation and use.
+ */
+ rmb();
+ opcode = READ_ONCE(cqe64->op_own) >> 4;
+ sop_qpn = ntohl(READ_ONCE(cqe64->sop_drop_qpn));
+ qpn = sop_qpn & 0xffffff;
+ sop = sop_qpn >> 24;
+ wqe_ctr = be16_to_cpu(READ_ONCE(cqe64->wqe_counter));
+
+ /*
+ * This poll_one is installed before the UMR QP is published, so a CQE
+ * seen while the QP is NULL is forged. Teardown stops polling before
+ * the QP is destroyed.
+ */
+ ibqp = smp_load_acquire(&dev->umrc.qp);
+ if (unlikely(!ibqp)) {
+ mlx5_ib_err(dev, "CQE on the UMR CQ before its QP is published\n");
+ goto forged_cqe;
+ }
+ qp = to_mqp(ibqp);
+ wq = &qp->sq;
+ idx = wqe_ctr & (wq->wqe_cnt - 1);
+
+ if (unlikely((opcode != MLX5_CQE_REQ && opcode != MLX5_CQE_REQ_ERR) ||
+ qpn != umrc->qpn || sop != MLX5_OPCODE_UMR)) {
+ mlx5_ib_err(dev,
+ "Unexpected CQE on UMR CQ: opcode 0x%x sop 0x%x qpn 0x%x\n",
+ opcode, sop, qpn);
+ goto forged_cqe;
+ }
+
+ /* Pairs with smp_store_release() in mlx5r_umr_post_send() */
+ head = umrc->pending_head;
+ expected = smp_load_acquire(&umrc->pending[head & (MLX5_UMR_MAX_WR - 1)]);
+
+ if (unlikely(!expected)) {
+ mlx5_ib_err(dev,
+ "UMR CQE wqe_ctr 0x%x with an empty shadow FIFO, possible NIC forgery\n",
+ wqe_ctr);
+ goto forged_cqe;
+ }
+
+ if (unlikely(wqe_ctr != expected->wqe_ctr)) {
+ mlx5_ib_err(dev,
+ "UMR wqe_ctr 0x%x mismatch with expected context, possible NIC forgery\n",
+ wqe_ctr);
+ goto forged_cqe;
+ }
+
+ /*
+ * A WQE records wq->head when posted, and mlx5r_ring_db() then advances
+ * wq->head. If they are still equal, mlx5r_ring_db() was not reached
+ * yet, so the WQE was never handed to the NIC.
+ */
+ if (unlikely(wq->wqe_head[idx] == READ_ONCE(wq->head))) {
+ mlx5_ib_err(dev,
+ "UMR CQE wqe_ctr 0x%x for an uncommitted WQE, possible NIC forgery\n",
+ wqe_ctr);
+ goto forged_cqe;
+ }
+
+ if (opcode == MLX5_CQE_REQ) {
+ wc->status = IB_WC_SUCCESS;
+ } else {
+ /* Copy the CQE, so its syndrome is read once */
+ err_cqe = *(struct mlx5_err_cqe *)cqe64;
+ mlx5_handle_error_cqe(dev, &err_cqe, wc);
+ /*
+ * A real failure during a recovery is forged, the QP
+ * only flushes.
+ */
+ if (wc->status != IB_WC_WR_FLUSH_ERR &&
+ cmpxchg(&umrc->state, MLX5_UMR_STATE_ACTIVE,
+ MLX5_UMR_STATE_RECOVER) != MLX5_UMR_STATE_ACTIVE) {
+ mlx5_ib_err(dev,
+ "UMR error CQE status %d in state %d, possible NIC forgery\n",
+ wc->status, READ_ONCE(umrc->state));
+ goto forged_cqe;
+ }
+ }
+
+ /* Pop the shadow FIFO head: validated, dispatch to its waiter */
+ WRITE_ONCE(umrc->pending[head & (MLX5_UMR_MAX_WR - 1)], NULL);
+ umrc->pending_head = head + 1;
+ wc->wr_cqe = &expected->cqe;
+
+ wc->qp = &qp->ibqp;
+ wq->tail = wq->wqe_head[idx] + 1;
+ /* Advance wq->last_poll, the internal error flush starts from it */
+ handle_atomics(qp, cqe64, wq->last_poll, idx);
+
+ return 0;
+
+forged_cqe:
+ /* mlx5_ib_poll_cq() updates the CI only for a returned completion */
+ mlx5_cq_set_ci(&cq->mcq);
+ /*
+ * The NIC forged a CQE. Fail the UMR path for good, ignore any later
+ * CQE and force the health recovery, as the fatal sensors still read
+ * clean. The internal error flush then completes the waiters. Return
+ * an error, not -EAGAIN, as a CQE was consumed.
+ */
+ umrc->state = MLX5_UMR_STATE_ERR;
+ cq->poll_one = mlx5_ib_poll_one_stopped;
+ mlx5_trigger_health_work_force(dev->mdev);
+ return -EIO;
+}
+
int mlx5_ib_poll_cq(struct ib_cq *ibcq, int num_entries, struct ib_wc *wc)
{
struct mlx5_ib_cq *cq = to_mcq(ibcq);
diff --git a/drivers/infiniband/hw/mlx5/mlx5_ib.h b/drivers/infiniband/hw/mlx5/mlx5_ib.h
index 9cc4b2f99b69..f7c718e3d974 100644
--- a/drivers/infiniband/hw/mlx5/mlx5_ib.h
+++ b/drivers/infiniband/hw/mlx5/mlx5_ib.h
@@ -768,6 +768,8 @@ struct umr_common {
struct ib_pd *pd;
struct ib_cq *cq;
struct ib_qp *qp;
+ /* Checked against each CQE in adversarial_mode */
+ u32 qpn;
/* Protects from UMR QP overflow
*/
struct semaphore sem;
@@ -1311,6 +1313,9 @@ int mlx5_ib_create_user_cq(struct ib_cq *ibcq,
struct uverbs_attr_bundle *attrs);
int mlx5_ib_destroy_cq(struct ib_cq *cq, struct ib_udata *udata);
int mlx5_ib_poll_cq(struct ib_cq *ibcq, int num_entries, struct ib_wc *wc);
+int mlx5_ib_poll_one_umr(struct mlx5_ib_cq *cq, struct mlx5_ib_qp **cur_qp,
+ struct ib_wc *wc);
+void mlx5_ib_cq_stop_polling(struct ib_cq *ibcq);
int mlx5_ib_pre_destroy_cq(struct ib_cq *cq);
void mlx5_ib_post_destroy_cq(struct ib_cq *cq);
int mlx5_ib_arm_cq(struct ib_cq *ibcq, enum ib_cq_notify_flags flags);
diff --git a/drivers/infiniband/hw/mlx5/umr.c b/drivers/infiniband/hw/mlx5/umr.c
index aba79a19d627..b0f00d4bc93c 100644
--- a/drivers/infiniband/hw/mlx5/umr.c
+++ b/drivers/infiniband/hw/mlx5/umr.c
@@ -194,6 +194,23 @@ int mlx5r_umr_resource_init(struct mlx5_ib_dev *dev)
sema_init(&dev->umrc.sem, MAX_UMR_WR);
mutex_init(&dev->umrc.lock);
dev->umrc.state = MLX5_UMR_STATE_ACTIVE;
+
+ if (mlx5_ib_adversarial_mode) {
+ struct mlx5_ib_cq *mcq = to_mcq(cq);
+ unsigned long flags;
+
+ dev->umrc.qpn = qp->qp_num;
+ /*
+ * UMRs can be posted as soon as the QP is published below, by
+ * this caller or by a concurrent MR registration, so install
+ * mlx5_ib_poll_one_umr() first. The CQ is already armed, so
+ * swap it under cq->lock, which the poll loop holds.
+ */
+ spin_lock_irqsave(&mcq->lock, flags);
+ mcq->poll_one = mlx5_ib_poll_one_umr;
+ spin_unlock_irqrestore(&mcq->lock, flags);
+ }
+
smp_store_release(&dev->umrc.qp, qp);
mutex_unlock(&dev->umrc.init_lock);
@@ -212,6 +229,14 @@ void mlx5r_umr_resource_cleanup(struct mlx5_ib_dev *dev)
{
if (dev->umrc.state == MLX5_UMR_STATE_UNINIT)
return;
+
+ /*
+ * Stop polling before the QP is destroyed, as its QPN may then be
+ * reused and a forged CQE carrying it must not be interpreted.
+ */
+ if (mlx5_ib_adversarial_mode)
+ mlx5_ib_cq_stop_polling(dev->umrc.cq);
+
mutex_destroy(&dev->umrc.lock);
/* After device init, UMR cp/qp are not unset during the lifetime. */
ib_destroy_qp(dev->umrc.qp);
@@ -319,6 +344,11 @@ static int mlx5r_umr_recover(struct mlx5_ib_dev *dev, u32 mkey,
int err;
mutex_lock(&umrc->lock);
+ if (mlx5_ib_adversarial_mode && umrc->state == MLX5_UMR_STATE_ERR) {
+ /* The NIC forged a CQE, don't revive the QP */
+ mutex_unlock(&umrc->lock);
+ return -EFAULT;
+ }
/* Preventing any further WRs to be sent now */
if (umrc->state != MLX5_UMR_STATE_RECOVER) {
mlx5_ib_warn(dev, "UMR recovery encountered an unexpected state=%d\n",
@@ -425,6 +455,14 @@ static int mlx5r_umr_post_send_wait(struct mlx5_ib_dev *dev, u32 mkey,
if (umr_context.status == IB_WC_WR_FLUSH_ERR)
continue;
+ if (mlx5_ib_adversarial_mode) {
+ /* A forged CQE was detected, don't recover the QP */
+ if (umrc->state == MLX5_UMR_STATE_ERR) {
+ err = -EFAULT;
+ break;
+ }
+ }
+
WARN_ON_ONCE(1);
mlx5_ib_warn(dev,
"reg umr failed (%u). Trying to recover and resubmit the flushed WQEs, mkey = %u\n",
--
2.18.1
next prev parent reply other threads:[~2026-11-10 13:45 UTC|newest]
Thread overview: 25+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-10 13:42 [PATCH V1 rdma-next 00/24] mlx5: Adversarial mode for confidential computing guests Yishai Hadas
2026-11-10 13:42 ` [PATCH V1 rdma-next 01/24] net/mlx5: Add mlx5_trigger_health_work_force() Yishai Hadas
2026-11-10 13:42 ` [PATCH V1 rdma-next 02/24] net/mlx5: Derive adversarial mode from CC guest memory encryption Yishai Hadas
2026-11-10 13:42 ` [PATCH V1 rdma-next 03/24] net/mlx5: Add capability filtering in adversarial mode Yishai Hadas
2026-11-10 13:43 ` [PATCH V1 rdma-next 04/24] net/mlx5: Catch too low max number of EQs Yishai Hadas
2026-11-10 13:43 ` [PATCH V1 rdma-next 05/24] net/mlx5: Add macros for reading log-based caps Yishai Hadas
2026-11-10 13:43 ` [PATCH V1 rdma-next 06/24] RDMA/mlx5: Reject QP transport types not enabled by FW capability Yishai Hadas
2026-11-10 13:43 ` [PATCH V1 rdma-next 07/24] RDMA/mlx5: Require IB_QPT_DRIVER for the DC vendor QP types Yishai Hadas
2026-11-10 13:43 ` [PATCH V1 rdma-next 08/24] RDMA/mlx5: Derive mlx5_ib_adversarial_mode global from mlx5_core adversarial API Yishai Hadas
2026-11-10 13:43 ` [PATCH V1 rdma-next 09/24] RDMA/core: Suppress kverbs_provider for adversarial_mode devices Yishai Hadas
2026-11-10 13:43 ` [PATCH V1 rdma-next 10/24] RDMA/mlx5: Remove duplicate mlx5_ib_umr_context struct Yishai Hadas
2026-11-10 13:43 ` [PATCH V1 rdma-next 11/24] RDMA/mlx5: Introduce per-CQ poll_one hook in mlx5_ib_cq Yishai Hadas
2026-11-10 13:43 ` [PATCH V1 rdma-next 12/24] RDMA/mlx5: Add shadow FIFO infrastructure for UMR completions Yishai Hadas
2026-11-10 13:43 ` Yishai Hadas [this message]
2026-11-10 13:43 ` [PATCH V1 rdma-next 14/24] RDMA/mlx5: Keep the UMR QP failed once a forged CQE is detected Yishai Hadas
2026-11-10 13:43 ` [PATCH V1 rdma-next 15/24] RDMA/mlx5: Block generic CQ poll path in adversarial mode Yishai Hadas
2026-11-10 13:43 ` [PATCH V1 rdma-next 16/24] RDMA/core: Set the CQ comp_handler after its poll context is set up Yishai Hadas
2026-11-10 13:43 ` [PATCH V1 rdma-next 17/24] RDMA/mlx5: Ignore completion events for a kernel CQ that is not ready Yishai Hadas
2026-11-10 13:43 ` [PATCH V1 rdma-next 18/24] RDMA/mlx5: Validate EQE port and GSI port before indexing devr.ports[] Yishai Hadas
2026-11-10 13:43 ` [PATCH V1 rdma-next 19/24] RDMA/mlx5: Validate firmware RQ/SQ state before sqrq_trans[] lookup Yishai Hadas
2026-11-10 13:43 ` [PATCH V1 rdma-next 20/24] RDMA/mlx5: Clamp log_max_cq_sz shift in CQ size checks Yishai Hadas
2026-11-10 13:43 ` [PATCH V1 rdma-next 21/24] RDMA/mlx5: Clamp log_max_* shifts in QP and WQ limit checks Yishai Hadas
2026-11-10 13:43 ` [PATCH V1 rdma-next 22/24] RDMA/mlx5: Clamp log_max_* shifts in query_device and ucontext paths Yishai Hadas
2026-11-10 13:43 ` [PATCH V1 rdma-next 23/24] RDMA/core: Report and enforce adversarial mode via GET_CONTEXT Yishai Hadas
2026-11-10 13:43 ` [PATCH V1 rdma-next 24/24] RDMA/mlx5: Advertise adversarial mode via ib_device::adversarial_mode Yishai Hadas
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261110134320.34408-14-yishaih@nvidia.com \
--to=yishaih@nvidia.com \
--cc=andrew+netdev@lunn.ch \
--cc=davem@davemloft.net \
--cc=dtatulea@nvidia.com \
--cc=edumazet@kernel.org \
--cc=jgg@ziepe.ca \
--cc=kuba@kernel.org \
--cc=leon@kernel.org \
--cc=linux-rdma@vger.kernel.org \
--cc=maorg@nvidia.com \
--cc=mbloch@nvidia.com \
--cc=moshe@nvidia.com \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
--cc=saeedm@nvidia.com \
--cc=tariqt@nvidia.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox