BPF List
 help / color / mirror / Atom feed
From: Maciej Fijalkowski <maciej.fijalkowski@intel.com>
To: Jakub Kicinski <kuba@kernel.org>
Cc: <netdev@vger.kernel.org>, <bpf@vger.kernel.org>,
	<magnus.karlsson@intel.com>, <stfomichev@gmail.com>,
	<pabeni@redhat.com>, <horms@kernel.org>, <bjorn@kernel.org>,
	<kerneljasonxing@gmail.com>, <kernelxing@tencent.com>
Subject: Re: [PATCH v4 net 4/6] xsk: reclaim invalid Tx descriptors in ZC batch path
Date: Fri, 24 Jul 2026 16:49:41 +0200	[thread overview]
Message-ID: <amN7hSwJZquAYv+Y@boxer> (raw)
In-Reply-To: <20260723162947.747377-1-kuba@kernel.org>

On Thu, Jul 23, 2026 at 09:29:47AM -0700, Jakub Kicinski wrote:
> This is an AI-generated review of your patch. The human sending this
> email has considered the AI review valid, or at least plausible.

Ok, see inline. All the 'high' issues are IMHO not valid.
Mediums seem to not be show-stoppers. Two other patch responses land in
the same category I'd say.

> ---
> xsk: reclaim invalid Tx descriptors in ZC batch path
> 
> This change parses ZC Tx batches in packet units so that descriptors
> belonging to invalid or oversize packets are appended to the CQ address
> area for userspace reclaim instead of being silently consumed. New
> per-pool state (reclaim_descs, tx_zc_pending_descs) sequences reclaim
> publication against outstanding driver-visible Tx completions.
> 
> Fixes: cf24f5a5feea ("xsk: add support for AF_XDP multi-buffer on Tx path")
> 
> > diff --git a/Documentation/networking/af_xdp.rst b/Documentation/networking/af_xdp.rst
> > index 50d92084a49c..cc3f0d16b28f 100644
> > --- a/Documentation/networking/af_xdp.rst
> > +++ b/Documentation/networking/af_xdp.rst
> > @@ -504,21 +506,25 @@ will be treated as an invalid descriptor.
> >  These are the semantics for producing packets onto AF_XDP Tx ring
> >  consisting of multiple frames:
> >  
> > -* When an invalid descriptor is found, all the other
> > -  descriptors/frames of this packet are marked as invalid and not
> > -  completed. The next descriptor is treated as the start of a new
> > -  packet, even if this was not the intent (because we cannot guess
> > -  the intent). As before, if your program is producing invalid
> > -  descriptors you have a bug that must be fixed.
> > +* When an invalid descriptor is found, the complete packet is treated as
> > +  invalid. The kernel consumes descriptors through the descriptor marking
> > +  the end of the packet and returns all their frame addresses through the
> > +  COMPLETION ring. A standalone invalid descriptor is treated as a
> > +  one-descriptor invalid packet. The descriptor following the end of the
> > +  invalid packet is treated as the start of a new packet. As before, if
> > +  your program is producing invalid descriptors you have a bug that must
> > +  be fixed. Rejected descriptors are reported in the ``tx_invalid_descs``
> > +  statistic.
> 
> [Medium]
> Is the "Rejected descriptors are reported in the tx_invalid_descs
> statistic" claim accurate for the oversize case?
> 
> Looking at xskq_cons_read_desc_batch(), the xdp_zc_max_segs overflow path
> sets drain based on nr_frags alone:
> 
>     if (nr_frags == pool->xdp_zc_max_segs)
>         drain = true;
> 
> Every individual descriptor in the oversize packet passed
> xskq_cons_is_valid_desc(), so invalid_descs (bumped only from that
> per-descriptor helper) is never incremented in this path.
> 
> Should the documentation distinguish the oversize case, or should the
> oversize drain also increment tx_invalid_descs so the documented
> observability signal matches reality?
> 
> > @@ -504,21 +506,25 @@ will be treated as an invalid descriptor.
> >  * For copy mode, the maximum supported number of frames in a packet is
> > -  equal to CONFIG_MAX_SKB_FRAGS + 1. If it is exceeded, all
> > -  descriptors accumulated so far are dropped and treated as
> > -  invalid. To produce an application that will work on any system
> > -  regardless of this config setting, limit the number of frags to 18,
> > -  as the minimum value of the config is 17.
> > +  equal to CONFIG_MAX_SKB_FRAGS + 1. If it is exceeded, all descriptors
> > +  through the end of the oversized packet are consumed, treated as invalid,
> > +  and their frame addresses are returned through the COMPLETION ring. To
> > +  produce an application that will work on any system regardless of this
> > +  config setting, limit the number of frags to 18, as the minimum value of
> > +  the config is 17.
> 
> [ ... ]
> 
> > diff --git a/include/net/xsk_buff_pool.h b/include/net/xsk_buff_pool.h
> > index f5e737a83055..2bb1d122b1bc 100644
> > --- a/include/net/xsk_buff_pool.h
> > +++ b/include/net/xsk_buff_pool.h
> > @@ -78,6 +78,9 @@ struct xsk_buff_pool {
> >  	u32 chunk_size;
> >  	u32 chunk_shift;
> >  	u32 frame_len;
> > +	u32 tx_descs_nentries;
> > +	u32 reclaim_descs;
> > +	u32 tx_zc_pending_descs;
> >  	u32 xdp_zc_max_segs;
> 
> [High]
> How are pool->reclaim_descs and pool->tx_zc_pending_descs unwound when
> the Tx path stops without producing further completions?
> 
> The gate in xsk_tx_peek_release_desc_batch() is:
> 
>     if (unlikely(READ_ONCE(pool->reclaim_descs)))
>         goto out;
> 
> The only place these two fields are cleared is xsk_tx_completed() when
> enough driver-visible completions arrive. What happens if:
> 
>     - the netdev is brought down via ndo_stop mid-flight,
>     - the socket unbinds/closes while reclaim_descs is nonzero,
>     - the ZC driver resets and drops in-flight descriptors,
>     - link goes down and the pending Tx never completes?

These cases are covered via xsk_notifier and/or xp_disable_drv_zc where
eventually we hit ZC driver that will release pending descs holding umem
addrs via xsk_tx_completed(), see ice_xsk_clean_xdp_ring().

> 
> In each of these cases xsk_tx_completed() is never called with the
> outstanding count, pool->reclaim_descs stays nonzero forever, and every
> subsequent xsk_tx_peek_release_desc_batch() early-returns. The reclaim
> CQ slots that xskq_prod_write_addr_batch() has already written are never
> made visible to userspace because the CQ producer is never advanced.
> 
> In a shared-UMEM pool, one socket tearing down mid-drain would block ZC
> batch Tx for every peer socket on that pool. There does not seem to be
> any teardown path (xsk_release, xp_disable_drv_zc/xp_enable_drv_zc,
> driver rebind) that zeroes these new fields.

No. If mid-drain means we did not hit EOP desc in current batch, there are
two cases:
- no descs given to HW, but reclaim_descs != 0 -> this is published to CQ
  immediately via xsk_tx_completed(pool, 0); in xsk_tx_commit_batch()
- tx_zc_pending_descs != 0, reclaim_descs != 0 -> reclaim descs will be
  published after HW is done with tx_zc_pending_descs and once Tx irq is
  cleaned, which will call xsk_tx_completed() for us.

> 
> Similarly, xs->drain_cont is per-socket state that can persist across
> close. If the socket goes away mid-drain, is anything expected to reset
> the pool-level counters that were set on its behalf?
> 
> > diff --git a/net/xdp/xsk.c b/net/xdp/xsk.c
> > index 091792d1d82d..f906d51b6699 100644
> > --- a/net/xdp/xsk.c
> > +++ b/net/xdp/xsk.c
> > @@ -499,6 +499,23 @@ void __xsk_map_flush(struct list_head *flush_list)
> >  
> >  void xsk_tx_completed(struct xsk_buff_pool *pool, u32 nb_entries)
> >  {
> > +	u32 reclaim_descs = READ_ONCE(pool->reclaim_descs);
> > +
> > +	if (unlikely(reclaim_descs)) {
> > +		u32 pending_descs = READ_ONCE(pool->tx_zc_pending_descs);
> > +
> > +		if (nb_entries < pending_descs) {
> > +			WRITE_ONCE(pool->tx_zc_pending_descs,
> > +				   pending_descs - nb_entries);
> > +			xskq_prod_submit_n(pool->cq, nb_entries);
> > +			return;
> > +		}
> > +
> > +		WRITE_ONCE(pool->tx_zc_pending_descs, 0);
> > +		nb_entries += reclaim_descs;
> > +		WRITE_ONCE(pool->reclaim_descs, 0);
> > +	}
> > +
> >  	xskq_prod_submit_n(pool->cq, nb_entries);
> >  }
> >  EXPORT_SYMBOL(xsk_tx_completed);
> 
> [High]
> Is memory ordering between reclaim_descs and tx_zc_pending_descs
> sufficient here?
> 
> The writer in xsk_tx_commit_batch() publishes tx_zc_pending_descs first,
> then reclaim_descs:
> 
>     WRITE_ONCE(pool->tx_zc_pending_descs,
>                batch->tx_descs + cq_pending_descs);
>     WRITE_ONCE(pool->reclaim_descs, batch->reclaim_descs);
> 
> The reader here loads reclaim_descs first, then tx_zc_pending_descs.
> WRITE_ONCE / READ_ONCE prevent tearing and compiler reordering but do
> not emit CPU barriers.
> 
> On weakly-ordered architectures, if the reader observes
> reclaim_descs != 0 but a stale tx_zc_pending_descs == 0, the
> 
>     if (nb_entries < pending_descs)
> 
> branch is skipped, reclaim_descs is added to nb_entries, and the CQ is
> advanced past the driver-visible Tx slots that were supposed to precede
> the reclaim entries. This is the ordering the commit message says the
> new fields exist to enforce ("CQ is positional. Descriptors already
> written but not submitted must complete before any reclaim-only
> descriptors appended below").
> 
> Should the flag store use smp_store_release() paired with an
> smp_load_acquire() on the reader, or an explicit smp_wmb/smp_rmb between
> the two fields on each side?
> 
> [High]
> Is this sequence safe against concurrent xsk_tx_completed() callers on
> the same pool?

All of that regarding barriers and concurrency does not make sense. Single
NAPI instance serializes pool operations. Shared pool is only when sockets
are attached to same <netdev, qid> tuple. Therefore we can not have a
situation where CPU0 is in write side (xsk_tx_commit_batch()) and CPU1 is
at clean side (xsk_tx_completed()) at the same time.

Way more nasty issues would occur if stuff raised above would indeed be
valid...

> 
>     reclaim_descs = READ_ONCE(pool->reclaim_descs);
>     if (unlikely(reclaim_descs)) {
>         ...
>         WRITE_ONCE(pool->tx_zc_pending_descs, 0);
>         nb_entries += reclaim_descs;
>         WRITE_ONCE(pool->reclaim_descs, 0);
>     }
> 
> xsk_tx_completed() is EXPORT_SYMBOL and called by several drivers
> (i40e, ice, ixgbe, mlx5, virtio_net, gve, ...). virtio_net in
> particular can invoke it from more than one context.
> 
> If two callers both READ_ONCE(reclaim_descs) = R, both take the reclaim
> branch, and both add R to their local nb_entries before clearing
> reclaim_descs, do they not together publish 2*R reclaim entries on the
> CQ, releasing the same UMEM frame addresses to userspace twice?
> 
> The existing CQ producer paths (xsk_cq_reserve_locked /
> xsk_cq_cancel_locked) already take pool->cq->cq_cached_prod_lock
> precisely because the CQ can be produced from multiple contexts. Should
> the new RMW on reclaim_descs / tx_zc_pending_descs extend that lock
> coverage, or use an atomic RMW / cmpxchg?
> 
> > @@ -574,24 +591,157 @@ static u32 xsk_tx_peek_release_fallback(struct xsk_buff_pool *pool, u32 max_entr
> >  	return nb_pkts;
> >  }
> >  
> > +static void xsk_tx_commit_batch(struct xsk_buff_pool *pool,
> > +				struct xsk_tx_batch *batch)
> > +{
> > +	u32 nb_descs = xsk_tx_batch_cq_descs(batch);
> > +	u32 cq_cached_prod;
> > +
> > +	if (!nb_descs)
> > +		return;
> > +
> > +	cq_cached_prod = pool->cq->cached_prod;
> > +	xskq_prod_write_addr_batch(pool->cq, pool->tx_descs, nb_descs);
> > +
> > +	if (unlikely(batch->reclaim_descs)) {
> > +		u32 cq_pending_descs;
> > +
> > +		/* CQ is positional. Descriptors already written but not
> > +		 * submitted must complete before any reclaim-only descriptors
> > +		 * appended below.
> > +		 */
> > +		cq_pending_descs = cq_cached_prod - xskq_get_prod(pool->cq);
> > +
> > +		WRITE_ONCE(pool->tx_zc_pending_descs,
> > +			   batch->tx_descs + cq_pending_descs);
> > +		WRITE_ONCE(pool->reclaim_descs, batch->reclaim_descs);
> > +		if (unlikely(!pool->tx_zc_pending_descs))
> > +			xsk_tx_completed(pool, 0);
> > +	}
> > +}
> > +
> > +static struct xsk_tx_batch
> > +__xsk_tx_peek_release_desc_batch(struct xsk_buff_pool *pool, struct xdp_sock *xs,
> > +				 struct xdp_desc *descs, u32 max_descs)
> > +{
> > +	struct xsk_tx_batch batch = {};
> > +	u32 entries;
> > +
> > +	entries = xskq_cons_nb_entries(xs->tx, max_descs);
> > +	if (!entries)
> > +		return batch;
> > +
> > +	batch = xskq_cons_read_desc_batch(xs, pool, descs, max_descs);
> > +	if (!xsk_tx_batch_cq_descs(&batch)) {
> > +		xs->tx->queue_empty_descs++;
> > +	} else {
> > +		__xskq_cons_release(xs->tx);
> > +		xs->sk.sk_write_space(&xs->sk);
> > +	}
> > +	return batch;
> > +}
> 
> [Medium]
> Is queue_empty_descs the right counter to bump when the batch returned
> zero because of the budget_limited path?
> 
> xskq_cons_read_desc_batch() can return
> 
>     batch = { tx_descs = 0, reclaim_descs = 0, budget_limited = true }
> 
> when it reads descriptors mid-packet, hits nb_entries == max before the
> end-of-packet descriptor, and rewinds cached_cons. In that case the
> queue is not empty; the caller was just out of budget. Should this case
> be distinguished from a truly-empty consumer queue for the userspace
> statistic?
> 
> > @@ -603,19 +753,16 @@ u32 xsk_tx_peek_release_desc_batch(struct xsk_buff_pool *pool, u32 nb_pkts)
> > [ ... ]
> > +	batch = umem_shared ?
> > +		xsk_tx_peek_release_shared_desc_batch(pool, nb_pkts) :
> > +		__xsk_tx_peek_release_desc_batch(pool, xs,
> > +						 pool->tx_descs,
> > +						 nb_pkts);
> > +	xsk_tx_commit_batch(pool, &batch);
> 
> [High]
> xsk_tx_peek_release_desc_batch() checks pool->reclaim_descs under only
> rcu_read_lock():
> 
>     if (unlikely(READ_ONCE(pool->reclaim_descs)))
>         goto out;
> 
> and xsk_tx_commit_batch() later overwrites both fields unconditionally
> with WRITE_ONCE. If two producers pass the gating check concurrently
> (the batch API is not documented as single-threaded, and the pre-existing
> shared-UMEM CQ producer paths already require cq_cached_prod_lock), can
> one batch's pending-drain accounting silently overwrite the other's,
> causing reclaim entries to be published before the driver-visible
> descriptors from the other batch have completed?

Mixing copy and zero-copy mode here. Also see above.

> 
> > diff --git a/net/xdp/xsk_queue.h b/net/xdp/xsk_queue.h
> > index 3e3fbb73d23e..1bc42c8902f4 100644
> > --- a/net/xdp/xsk_queue.h
> > +++ b/net/xdp/xsk_queue.h
> > @@ -263,17 +274,18 @@ static inline void parse_desc(struct xsk_queue *q, struct xsk_buff_pool *pool,
> >  	parsed->mb = xp_mb_desc(desc);
> >  }
> >  
> > -static inline
> > -u32 xskq_cons_read_desc_batch(struct xsk_queue *q, struct xsk_buff_pool *pool,
> > -			      u32 max)
> > +static inline struct xsk_tx_batch
> > +xskq_cons_read_desc_batch(struct xdp_sock *xs, struct xsk_buff_pool *pool,
> > +			  struct xdp_desc *descs, u32 max)
> >  {
> [ ... ]
> >  		descs[nb_entries] = ring->desc[idx];
> >  		cached_cons++;
> >  		parse_desc(q, pool, &descs[nb_entries], &parsed);
> >  		if (unlikely(!parsed.valid))
> > -			break;
> > +			drain = true;
> > +
> > +		nr_frags++;
> > +		nb_entries++;
> 
> [Medium]
> descs[nb_entries] is populated from ring->desc[idx] before parse_desc()
> even for invalid descriptors, and xsk_tx_commit_batch() later writes
> descs[i].addr straight into the CQ via xskq_prod_write_addr_batch() for
> the reclaim range. No xp_aligned_validate_desc / xp_unaligned_validate_desc
> range check is applied to invalid-descriptor addr values before they
> appear on the CQ.

Invalid descs are now intentionally produced to CQ. That's the approach we
picked to avoid losing umem addresses that would cause potential buffer
starvation.

> 
> In XDP_SHARED_UMEM configurations where multiple sockets share a CQ, does
> this allow one socket to publish an arbitrary attacker-chosen 64-bit
> value into the shared CQ by submitting a single invalid Tx descriptor?
> 
> Downstream code that treats CQ entries as UMEM offsets (e.g. paths that
> feed them back through xp_raw_get_data on subsequent Tx) would then
> operate on that raw value. Prior semantics dropped invalid-descriptor
> addresses silently.
> 
> Should the reclaim CQ entry be sanitized (masked to a frame-aligned base
> or omitted) rather than published verbatim?

  reply	other threads:[~2026-07-24 14:49 UTC|newest]

Thread overview: 17+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-19 13:56 [PATCH v4 net 0/6] xsk: fix AF_XDP multi-buffer Tx descriptor reclaim Maciej Fijalkowski
2026-07-19 13:56 ` [PATCH v4 net 1/6] xsk: fix buffer leak in xsk_drop_skb() for AF_XDP multi-buffer Tx Maciej Fijalkowski
2026-07-19 13:56 ` [PATCH v4 net 2/6] xsk: drain continuation descs after overflow in xsk_build_skb() Maciej Fijalkowski
2026-07-20 13:58   ` sashiko-bot
2026-07-21 18:04   ` Maciej Fijalkowski
2026-07-22  3:09     ` Jason Xing
2026-07-23 16:29   ` Jakub Kicinski
2026-07-19 13:56 ` [PATCH v4 net 3/6] xsk: provide sufficient space in pool->tx_descs Maciej Fijalkowski
2026-07-20 13:58   ` sashiko-bot
2026-07-19 13:56 ` [PATCH v4 net 4/6] xsk: reclaim invalid Tx descriptors in ZC batch path Maciej Fijalkowski
2026-07-23 16:29   ` Jakub Kicinski
2026-07-24 14:49     ` Maciej Fijalkowski [this message]
2026-07-19 13:56 ` [PATCH v4 net 5/6] selftests/xsk: fix too-many-frags multi-buffer Tx test Maciej Fijalkowski
2026-07-19 13:56 ` [PATCH v4 net 6/6] selftests/xsk: account reclaimed invalid Tx descriptors Maciej Fijalkowski
2026-07-23 16:29   ` Jakub Kicinski
2026-07-20 19:30 ` [PATCH v4 net 0/6] xsk: fix AF_XDP multi-buffer Tx descriptor reclaim Stanislav Fomichev
2026-07-23 16:31 ` Jakub Kicinski

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=amN7hSwJZquAYv+Y@boxer \
    --to=maciej.fijalkowski@intel.com \
    --cc=bjorn@kernel.org \
    --cc=bpf@vger.kernel.org \
    --cc=horms@kernel.org \
    --cc=kerneljasonxing@gmail.com \
    --cc=kernelxing@tencent.com \
    --cc=kuba@kernel.org \
    --cc=magnus.karlsson@intel.com \
    --cc=netdev@vger.kernel.org \
    --cc=pabeni@redhat.com \
    --cc=stfomichev@gmail.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