> 
> This is an AI-generated review of your patch. The human sending this email has
> considered the AI review valid, or at least plausible.
> ---
> net: mana: RCU-protect gc->cq_table lookups against concurrent CQ destroy
> 
> This puts gc->cq_table (base pointer and entries) under RCU so the EQ
> interrupt handler can no longer dereference a CQ that a concurrent teardown
> has freed.  Teardown clears the slot and takes a grace period before the CQ
> memory is released, and the Ethernet paths are converted to a two-pass
> quiesce/free so the grace periods are batched instead of one per CQ.
> 
> A few questions below.

I'm sending V7 addressing all the issues pointed out in this review, together 
with Leon's suggestion on rearrange CQ and adding refcounts.

Thanks,

Long


> 
> > diff --git a/drivers/infiniband/hw/mana/cq.c
> > b/drivers/infiniband/hw/mana/cq.c index f2547989f4229..73d97b2f5cf9b
> > 100644
> > --- a/drivers/infiniband/hw/mana/cq.c
> > +++ b/drivers/infiniband/hw/mana/cq.c
> 
> [ ... ]
> 
> > @@ -149,23 +157,54 @@ int mana_ib_install_cq_cb(struct mana_ib_dev
> *mdev, struct mana_ib_cq *cq)
> >     gdma_cq->type = GDMA_CQ;
> >     gdma_cq->cq.callback = mana_ib_cq_handler;
> >     gdma_cq->id = cq->queue.id;
> > -   gc->cq_table[cq->queue.id] = gdma_cq;
> > +   rcu_assign_pointer(cq_table[cq->queue.id], gdma_cq);
> >     return 0;
> >  }
> >
> >  void mana_ib_remove_cq_cb(struct mana_ib_dev *mdev, struct
> mana_ib_cq
> > *cq)  {
> >     struct gdma_context *gc = mdev_to_gc(mdev);
> > +   struct gdma_queue __rcu **cq_table;
> > +   struct gdma_queue *gdma_cq;
> >
> > -   if (cq->queue.id >= gc->max_num_cqs || cq->queue.id ==
> INVALID_QUEUE_ID)
> > +   if (cq->queue.id == INVALID_QUEUE_ID)
> >             return;
> >
> >     if (cq->queue.kmem)
> >     /* Then it will be cleaned and removed by the mana */
> >             return;
> >
> > -   kfree(gc->cq_table[cq->queue.id]);
> > -   gc->cq_table[cq->queue.id] = NULL;
> > +   /* No rcu_read_lock(): like mana_ib_install_cq_cb(), this runs within
> > +    * the IB device lifetime that mana_rdma_remove() drains before the
> > +    * base cq_table can be freed.  See gdma_context::cq_table in gdma.h.
> > +    */
> > +   cq_table = rcu_dereference_protected(gc->cq_table, true);
> > +   if (!cq_table || cq->queue.id >= gc->max_num_cqs)
> > +           return;
> > +   /* Removers for a given CQ are serialized by the IB core, so the slot
> > +    * is read and cleared without rcu_read_lock() or atomicity: a CQ is
> > +    * never torn down while a live QP references it (cq->usecnt), nor
> > +    * while the QP-create that installed the entry is still running (that
> > +    * create holds a reference on the CQ uobject across its error path,
> > +    * before usecnt is taken).  Any double-remove is therefore sequential
> > +    * -- the later caller sees the NULL stored below and returns.
> > +    */
> > +   gdma_cq = rcu_dereference_protected(cq_table[cq->queue.id], true);
> > +   /* Clear the slot only if it still holds the entry this CQ installed
> > +    * (gdma_cq->cq.context == cq).  If the id was already removed, or was
> > +    * recycled and republished for another CQ, leave the current entry
> > +    * intact instead of wiping a live one.
> > +    */
> > +   if (!gdma_cq || gdma_cq->cq.context != cq)
> > +           return;
> > +
> > +   rcu_assign_pointer(cq_table[cq->queue.id], NULL);
> 
> [Severity: Low]
> The comment says the identity test protects against an id that "was recycled
> and republished for another CQ", but the compare and the clear are two
> separate accesses with nothing making them indivisible.  Both
> mana_ib_remove_cq_cb() here and mana_gd_unpublish_cq() do:
> 
>     /* read */
>     gdma_cq = rcu_dereference_protected(cq_table[cq->queue.id], true);
>     if (!gdma_cq || gdma_cq->cq.context != cq)
>             return;
>     /* separate store */
>     rcu_assign_pointer(cq_table[cq->queue.id], NULL);
> 
> The two sides of the table are also asymmetric: mana_ib_install_cq_cb()
> refuses to publish into a non-NULL slot, but mana_create_txq() and
> mana_create_rxq() publish with rcu_assign_pointer(cq_table[cq->gdma_id],
> cq->gdma_cq) and no occupancy check at all.
> 
> So can this interleaving wipe a live entry?
> 
>     remover                              publisher
>     reads slot, sees A
>                                          stores B' into the same slot
>     stores NULL                          (B' is now gone)
> 
> Since mana_gd_process_eqe() now drops a NULL slot silently, would every
> completion for B' then be discarded with no diagnostic, and would B''s own
> remover later see NULL and skip its kfree()?  Would a cmpxchg on the slot, or 
> a
> small lock covering cq_table entry mutation, make the conditional clear 
> actually
> atomic?
> 
> This does need the device to hand the same CQ id to two live consumers, so I
> could not demonstrate it in practice; the pre-patch code cleared the slot
> unconditionally, so the guard is still an improvement.
> 
> > +
> > +   /* Wait for in-flight EQ handlers that may have loaded the old
> > +    * pointer via rcu_dereference() to finish before freeing.
> > +    */
> > +   synchronize_rcu();
> > +   kfree(gdma_cq);
> >  }
> >
> >  int mana_ib_arm_cq(struct ib_cq *ibcq, enum ib_cq_notify_flags flags)
> > diff --git a/drivers/net/ethernet/microsoft/mana/gdma_main.c
> > b/drivers/net/ethernet/microsoft/mana/gdma_main.c
> > index e8b7ffb47eb98..d40f25a1a74a7 100644
> > --- a/drivers/net/ethernet/microsoft/mana/gdma_main.c
> > +++ b/drivers/net/ethernet/microsoft/mana/gdma_main.c
> 
> [ ... ]
> 
> > @@ -743,11 +744,30 @@ static void mana_gd_process_eqe(struct
> gdma_queue *eq)
> >     switch (type) {
> >     case GDMA_EQE_COMPLETION:
> >             cq_id = eqe->details[0] & 0xFFFFFF;
> > +           cq_table = rcu_dereference(gc->cq_table);
> > +           if (WARN_ON_ONCE(!cq_table))
> > +                   break;
> > +
> > +           /* Pair with the rcu_assign_pointer(gc->cq_table) release in
> > +            * mana_hwc_establish_channel(), which publishes the table
> > +            * after storing gc->max_num_cqs.  The rmb keeps this bound
> > +            * read ordered after the table load, so a shrinking
> > +            * re-establish cannot pair a stale, larger max_num_cqs with a
> > +            * newly published, smaller table and index out of bounds.
> > +            */
> > +           smp_rmb();
> >             if (WARN_ON_ONCE(cq_id >= gc->max_num_cqs))
> >                     break;
> >
> > -           cq = gc->cq_table[cq_id];
> > -           if (WARN_ON_ONCE(!cq || cq->type != GDMA_CQ || cq->id !=
> cq_id))
> > +           cq = rcu_dereference(cq_table[cq_id]);
> 
> [Severity: High]
> This isn't a bug introduced by this patch, but at this commit the bound used
> here is still mutable while a table is published.
> mana_hwc_init_event_handler() writes it straight from EQ interrupt context
> from a device-supplied value:
> 
>     case HWC_INIT_DATA_MAX_NUM_CQS:
>             gd->gdma_context->max_num_cqs = val;
> 
> while the table itself is sized once in mana_hwc_establish_channel() with
> vcalloc(gc->max_num_cqs, sizeof(*cq_table)).  A later, larger value would
> inflate every bound test (cq_id >= gc->max_num_cqs here, cq->gdma_id >=
> gc->max_num_cqs in mana_create_txq()/mana_create_rxq(), cq->queue.id >=
> gc->max_num_cqs in mana_ib_install_cq_cb()) past the allocation, and a
> smaller one would make mana_gd_unpublish_cq() return false so
> mana_gd_destroy_cq() skips the grace period for a still-published CQ.
> 
> The gdma.h comment added by this patch states the invariant as already
> holding ("max_num_cqs above is the size of cq_table and an upper bound on
> valid CQ indices for the table's lifetime"), which only becomes true with the 
> last
> patch in this series, "net: mana: keep max_num_cqs immutable once cq_table
> is allocated" -- that one moves the handler to WRITE_ONCE(hwc-
> >hwc_init_max_num_cqs, val) and has
> mana_hwc_establish_channel() commit a single READ_ONCE() snapshot as
> both the vcalloc() size and gc->max_num_cqs.  Should the documented
> contract land together with the change that establishes it, or reference it?
> 
> > +           /* A NULL entry is expected while a concurrent teardown
> > +            * (e.g. ifdown or an MTU change) has unpublished this CQ but
> > +            * not yet freed it; the completion is stale, so drop it
> > +            * quietly rather than warning.
> > +            */
> > +           if (!cq)
> > +                   break;
> > +           if (WARN_ON_ONCE(cq->type != GDMA_CQ || cq->id !=
> cq_id))
> >                     break;
> >
> >             if (cq->cq.callback)
> > @@ -1050,18 +1070,47 @@ static void mana_gd_create_cq(const struct
> gdma_queue_spec *spec,
> >     queue->cq.callback = spec->cq.callback;  }
> >
> > -static void mana_gd_destroy_cq(struct gdma_context *gc,
> > -                          struct gdma_queue *queue)
> > +bool mana_gd_unpublish_cq(struct gdma_context *gc, struct gdma_queue
> > +*queue)
> >  {
> > +   struct gdma_queue __rcu **cq_table;
> >     u32 id = queue->id;
> >
> > -   if (id >= gc->max_num_cqs)
> > -           return;
> > +   /* No rcu_read_lock() here: unpublish runs only on the
> > +    * CQ-destroy/teardown path, where the base cq_table is stable.  See
> > +    * the lifecycle note on gdma_context::cq_table in gdma.h for why the
> > +    * "true" predicate is sound.
> > +    */
> > +   cq_table = rcu_dereference_protected(gc->cq_table, true);
> > +   if (!cq_table || id >= gc->max_num_cqs)
> > +           return false;
> > +
> > +   /* Clear the slot only if it still refers to this queue.  The
> > +    * Ethernet two-pass teardown unpublishes the same index twice, a
> > +    * grace period apart, and a CQ that legitimately recycled this id in
> > +    * between (e.g. a new RDMA CQ via mana_ib_install_cq_cb()) must
> not
> > +    * have its fresh entry wiped by the second pass.
> > +    */
> > +   if (rcu_access_pointer(cq_table[id]) != queue)
> > +           return false;
> > +
> > +   rcu_assign_pointer(cq_table[id], NULL);
> > +   return true;
> > +}
> >
> > -   if (!gc->cq_table[id])
> > +static void mana_gd_destroy_cq(struct gdma_context *gc,
> > +                          struct gdma_queue *queue)
> > +{
> > +   /* A batched teardown may already have cleared the slot and taken
> the
> > +    * grace period; then there is nothing left to wait for.
> > +    */
> > +   if (!mana_gd_unpublish_cq(gc, queue))
> >             return;
> >
> > -   gc->cq_table[id] = NULL;
> > +   /* Wait for in-flight EQ handlers that may have loaded the old
> > +    * pointer via rcu_dereference() to finish before the caller
> > +    * frees the CQ memory.
> > +    */
> > +   synchronize_rcu();
> >  }
> 
> [Severity: Medium]
> mana_gd_unpublish_cq() returns false for two different states, and
> mana_gd_destroy_cq() treats both as "nothing left to wait for":
> 
>   a) the slot is already NULL because a batched pass cleared it and
>      already took the grace period, and
> 
>   b) rcu_access_pointer(cq_table[id]) != queue, i.e. this queue's entry
>      was overwritten by another publisher and no grace period has been
>      taken for this pointer.
> 
> In case (b), mana_gd_destroy_queue() then kfree()s the queue with no wait.
> Can an in-flight mana_gd_process_eqe() that already did
> 
>     cq = rcu_dereference(cq_table[cq_id]);
> 
> on exactly that pointer go on to read cq->type / cq->id and call
> cq->cq.callback(cq->cq.context, cq) after the free?
> 
> One route into state (b): for an RDMA kernel (kmem) CQ,
> mana_ib_install_cq_cb() publishes the entry, mana_ib_remove_cq_cb()
> returns early for kmem ("Then it will be cleaned and removed by the mana"),
> so the entry stays published across mana_ib_gd_destroy_cq() which releases
> the CQ id on the device, and is only cleared later via
> mana_ib_destroy_queue() -> mana_gd_destroy_queue() ->
> mana_gd_destroy_cq().
> If the device hands that id to an Ethernet CQ being created in that window,
> mana_create_txq()/mana_create_rxq() overwrite the slot unconditionally, and
> the identity test above then fails.
> 
> Would it be safer to distinguish "we cleared our slot", "never published"
> and "our entry was overwritten", and still synchronize in the last case?
> 
> I could not confirm the device actually recycles ids inside that window, so 
> this
> may not be reachable in practice.
> 
> > diff --git a/drivers/net/ethernet/microsoft/mana/hw_channel.c
> > b/drivers/net/ethernet/microsoft/mana/hw_channel.c
> > index e3c24d50dad07..409e20caeccdc 100644
> > --- a/drivers/net/ethernet/microsoft/mana/hw_channel.c
> > +++ b/drivers/net/ethernet/microsoft/mana/hw_channel.c
> 
> [ ... ]
> 
> > @@ -698,11 +699,15 @@ static int mana_hwc_establish_channel(struct
> gdma_context *gc, u16 *q_depth,
> >     if (WARN_ON(cq->id >= gc->max_num_cqs))
> >             return -EPROTO;
> >
> > -   gc->cq_table = vcalloc(gc->max_num_cqs, sizeof(struct gdma_queue
> *));
> > -   if (!gc->cq_table)
> > +   cq_table = vcalloc(gc->max_num_cqs, sizeof(*cq_table));
> > +   if (!cq_table)
> >             return -ENOMEM;
> >
> > -   gc->cq_table[cq->id] = cq;
> > +   rcu_assign_pointer(cq_table[cq->id], cq);
> > +   /* Publish the fully-initialised table last; pairs with the
> > +    * rcu_dereference(gc->cq_table) in mana_gd_process_eqe().
> > +    */
> > +   rcu_assign_pointer(gc->cq_table, cq_table);
> >
> >     return 0;
> >  }
> 
> [ ... ]
> 
> > diff --git a/drivers/net/ethernet/microsoft/mana/mana_en.c
> > b/drivers/net/ethernet/microsoft/mana/mana_en.c
> > index 92bb55935c1c4..5d215981bba83 100644
> > --- a/drivers/net/ethernet/microsoft/mana/mana_en.c
> > +++ b/drivers/net/ethernet/microsoft/mana/mana_en.c
> > @@ -2427,12 +2427,18 @@ static void mana_deinit_txq(struct
> > mana_port_context *apc, struct mana_txq *txq)
> >
> >  static void mana_destroy_txq(struct mana_port_context *apc)  {
> > +   struct gdma_context *gc = apc->ac->gdma_dev->gdma_context;
> >     struct napi_struct *napi;
> >     int i;
> >
> >     if (!apc->tx_qp)
> >             return;
> >
> > +   /* Pass 1: quiesce each CQ on the device and clear its cq_table slot.
> > +    * Taking one grace period below for the whole port avoids up to
> > +    * apc->num_queues serialized synchronize_rcu() calls (one per CQ in
> > +    * mana_gd_destroy_cq()) under RTNL on every teardown.
> > +    */
> 
> [Severity: Low]
> This isn't a bug, but is "the whole port" accurate here?  RX and TX are 
> batched
> independently: mana_destroy_rxqs() unpublishes only the RX CQs and calls
> synchronize_rcu(), then mana_destroy_txq() unpublishes only the TX CQs and
> calls synchronize_rcu() again, and mana_detach() runs both back to
> back:
> 
>     mana_destroy_rxqs(apc);
>     mana_destroy_txq(apc);
> 
> The same pairing happens on the attach error path.  So each ifdown, MTU
> change or ring reconfigure takes two grace periods per port rather than one.
> 
> The commit message says the same thing:
> 
>     Clear all of a port's CQ slots first and take a single grace period
>     per teardown instead
> 
> and the comment in mana_destroy_rxqs() reads "One grace period for the
> whole port instead of one per RXQ".  Would "one grace period per RX batch
> and one per TX batch" describe the result more precisely?
> 
> >     for (i = 0; i < apc->num_queues; i++) {
> >             if (!apc->tx_qp[i])
> >                     continue;
> > @@ -2448,8 +2454,24 @@ static void mana_destroy_txq(struct
> mana_port_context *apc)
> >                     apc->tx_qp[i]->txq.napi_initialized = false;
> >             }
> >
> > -           if (apc->tx_qp[i]->tx_object != INVALID_MANA_HANDLE)
> > -                   mana_destroy_wq_obj(apc, GDMA_SQ, apc->tx_qp[i]-
> >tx_object);
> > +           if (apc->tx_qp[i]->tx_object != INVALID_MANA_HANDLE) {
> > +                   mana_destroy_wq_obj(apc, GDMA_SQ,
> > +                                       apc->tx_qp[i]->tx_object);
> > +                   apc->tx_qp[i]->tx_object = INVALID_MANA_HANDLE;
> > +           }
> > +
> > +           if (apc->tx_qp[i]->tx_cq.gdma_cq)
> > +                   mana_gd_unpublish_cq(gc, apc->tx_qp[i]-
> >tx_cq.gdma_cq);
> > +   }
> > +
> > +   synchronize_rcu();
> > +
> > +   /* Pass 2: the slots are clear, so mana_gd_destroy_cq() skips its own
> > +    * grace period; free the CQ, the TXQ and the queue pair.
> > +    */
> > +   for (i = 0; i < apc->num_queues; i++) {
> > +           if (!apc->tx_qp[i])
> > +                   continue;
> >
> >             mana_deinit_cq(apc, &apc->tx_qp[i]->tx_cq);
> >
> 
> [ ... ]
> 
> > @@ -2987,16 +3054,31 @@ static void mana_destroy_rxqs(struct
> mana_port_context *apc)
> >     struct mana_rxq *rxq;
> >     u32 rxq_idx;
> >
> > -   if (apc->rxqs) {
> > +   if (!apc->rxqs)
> > +           return;
> >
> > -           for (rxq_idx = 0; rxq_idx < apc->num_queues; rxq_idx++) {
> > -                   rxq = apc->rxqs[rxq_idx];
> > -                   if (!rxq)
> > -                           continue;
> > +   /* Pass 1: quiesce every RXQ's CQ and clear its cq_table slot. */
> > +   for (rxq_idx = 0; rxq_idx < apc->num_queues; rxq_idx++) {
> > +           rxq = apc->rxqs[rxq_idx];
> > +           if (!rxq)
> > +                   continue;
> >
> > -                   mana_destroy_rxq(apc, rxq, true);
> > -                   apc->rxqs[rxq_idx] = NULL;
> > -           }
> > +           mana_quiesce_rxq(apc, rxq, true);
> > +   }
> > +
> > +   /* One grace period for the whole port instead of one per RXQ. */
> > +   synchronize_rcu();
> 
> [ ... ]
> 
> > diff --git a/include/net/mana/gdma.h b/include/net/mana/gdma.h index
> > 0c395917b2144..0f591029d363b 100644
> > --- a/include/net/mana/gdma.h
> > +++ b/include/net/mana/gdma.h
> 
> [ ... ]
> 
> > @@ -496,6 +525,12 @@ int mana_gd_create_mana_wq_cq(struct
> gdma_dev
> > *gd,
> >
> >  void mana_gd_destroy_queue(struct gdma_context *gc, struct gdma_queue
> > *queue);
> >
> > +/* Clear a CQ's cq_table slot without waiting for a grace period.
> > +Batched
> > + * teardown paths clear several slots and then take a single
> > +synchronize_rcu();
> > + * single-CQ callers use mana_gd_destroy_cq() instead, which also waits.
> > + */
> > +bool mana_gd_unpublish_cq(struct gdma_context *gc, struct gdma_queue
> > +*queue);
> 
> [Severity: Low]
> This isn't a bug, but the comment directs includers of this header to an API
> they cannot reach: mana_gd_destroy_cq() is static in gdma_main.c and is not
> declared anywhere.  The reachable entry point is mana_gd_destroy_queue(),
> which is EXPORT_SYMBOL_NS("NET_MANA") and dispatches GDMA_CQ into
> the private helper.
> 
> mana_gd_unpublish_cq() itself is non-static and declared here, but has no
> EXPORT_SYMBOL_NS("NET_MANA") unlike its header-declared siblings, so a
> mana_ib user of it would not link; its only users (gdma_main.c and
> mana_en.c) are both in mana.ko.
> 
> Would keeping the declaration in a mana-private header, or exporting it and
> pointing the comment at mana_gd_destroy_queue(), avoid the situation this
> patch already runs into -- mana_ib_remove_cq_cb() open-coding the same
> unpublish-then-wait sequence with a different identity predicate (gdma_cq-
> >cq.context != cq) from the one in mana_gd_unpublish_cq()
> (rcu_access_pointer(cq_table[id]) != queue)?
> 
> >  int mana_gd_poll_cq(struct gdma_queue *cq, struct gdma_comp *comp,
> > int num_cqe);
> >
> >  void mana_gd_ring_cq(struct gdma_queue *cq, u8 arm_bit);
> --
> pw-bot: cr

Reply via email to