All of lore.kernel.org
 help / color / mirror / Atom feed
From: Jakub Kicinski <kuba@kernel.org>
To: cyy@cyyself.name
Cc: Jakub Kicinski <kuba@kernel.org>,
	sukhdeeps@marvell.com, andrew+netdev@lunn.ch,
	davem@davemloft.net, edumazet@google.com, pabeni@redhat.com,
	almasrymina@google.com, hawk@kernel.org,
	richardcochran@gmail.com, netdev@vger.kernel.org,
	bpf@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: Re: [PATCH net-next v3] net: atlantic: convert RX path to page_pool
Date: Mon, 17 Aug 2026 09:59:19 -0700	[thread overview]
Message-ID: <20260817165919.3422774-1-kuba@kernel.org> (raw)
In-Reply-To: <tencent_F771B81731B921458D6F7CBF82ECDA888E08@qq.com>

This is an AI-generated review of your patch. The human sending this
email says "Comment about PTP queue index looks legit, I haven't
investigated the rest."
---
net: atlantic: convert RX path to page_pool

This converts the atlantic RX buffers from hand-rolled high-order page
allocation plus a page-flip reuse scheme to page_pool fragments, registers
the RX (and PTP RX) xdp_rxq with MEM_TYPE_PAGE_POOL, and drops the
PageFlips/PageReuses/PageFrees ethtool counters that described the old
scheme.  A few questions below about the ownership transfers and about
what the commit message describes.

> net: atlantic: convert RX path to page_pool

[Severity: Low]
The subject and the body scope this to the RX path:

  "ensures every RX path either keeps the ring's fragment reference for
   reposting or hands it to the skb/xdp_buff for later recycling"

but the patch also changes TX completion behaviour in aq_ring_tx_clean(),
replacing xdp_return_frame_rx_napi() with xdp_return_frame().  That change
is only documented in an in-code comment.

The difference is real once this patch lands, because __xdp_return()
honours napi_direct only for MEM_TYPE_PAGE_POOL:

	case MEM_TYPE_PAGE_POOL:
		netmem = netmem_compound_head(netmem);
		if (napi_direct && xdp_return_frame_no_direct())
			napi_direct = false;
		page_pool_put_full_netmem(netmem_get_pp(netmem), netmem,
					  napi_direct);

so all XDP TX completions lose direct NAPI recycling, and frames arriving
via ndo_xdp_xmit() from a foreign pool are no longer pushed into that
pool's lock-free cache.  Could the commit message mention this TX-side
semantic change?

> diff --git a/drivers/net/ethernet/aquantia/atlantic/aq_ptp.c b/drivers/net/ethernet/aquantia/atlantic/aq_ptp.c
> index 558ac9237f75..3a40d986cd67 100644
> --- a/drivers/net/ethernet/aquantia/atlantic/aq_ptp.c
> +++ b/drivers/net/ethernet/aquantia/atlantic/aq_ptp.c

[ ... ]

> @@ -1192,12 +1193,23 @@ int aq_ptp_ring_alloc(struct aq_nic_s *aq_nic)
>  	if (err)
>  		goto err_exit_ptp_tx;
>  
> +	err = xdp_rxq_info_reg(&aq_ptp->ptp_rx.xdp_rxq, aq_nic->ndev,
> +			       rx_ring_idx, aq_ptp->napi.napi_id);
> +	if (err < 0)
> +		goto err_exit_ptp_rx;

[Severity: Low]
Is rx_ring_idx the right value for the queue_index argument here?  It comes
from aq_ptp_ring_idx(), which is documented in aq_ptp.h as a hardware
descriptor ring index:

	/* Index must to be 8 (8 TCs) or 16 (4 TCs).
	 * It depends from Traffic Class mode.
	 */
	static inline unsigned int aq_ptp_ring_idx(const enum aq_tc_mode tc_mode)

The peer call site in aq_vec_ring_alloc() passes the netdev queue index
instead:

		if (xdp_rxq_info_reg(&ring->xdp_rxq,
				     aq_nic->ndev, idx,
				     self->napi.napi_id) < 0) {

Note it passes idx, not idx_ring.  queue_index is exported to BPF as
xdp_md->rx_queue_index and is what AF_XDP compares against the bound queue
id, and 8 or 16 is outside the range the driver advertises with:

	err = netif_set_real_num_rx_queues(self->ndev,
					   self->aq_vecs * cfg->tcs);

__xdp_rxq_info_reg() stores the value verbatim without any range check:

	xdp_rxq->dev = dev;
	xdp_rxq->queue_index = queue_index;

The clean paths already treat the PTP ring as queue 0 via
skb_record_rx_queue(skb, is_ptp_ring ? 0 : AQ_NIC_RING2QMAP(...)), so
should this registration use 0 (or the netdev queue id) as well?

> diff --git a/drivers/net/ethernet/aquantia/atlantic/aq_ring.c b/drivers/net/ethernet/aquantia/atlantic/aq_ring.c
> index e1193c6719d9..9dd881710594 100644
> --- a/drivers/net/ethernet/aquantia/atlantic/aq_ring.c
> +++ b/drivers/net/ethernet/aquantia/atlantic/aq_ring.c

[ ... ]

> @@ -346,7 +284,11 @@ bool aq_ring_tx_clean(struct aq_ring_s *self)
>  			++self->stats.tx.packets;
>  			self->stats.tx.bytes += xdp_get_frame_len(buff->xdpf);
>  			u64_stats_update_end(&self->stats.tx.syncp);
> -			xdp_return_frame_rx_napi(buff->xdpf);
> +			/* Frames queued via ndo_xdp_xmit() may come from a
> +			 * page pool owned by another NAPI context: no direct
> +			 * recycling.
> +			 */
> +			xdp_return_frame(buff->xdpf);
>  		}

[Severity: High]
This isn't a regression introduced by this patch, but the pre-existing
ndo_xdp_xmit accounting in aq_xdp_xmit() becomes more expensive once the
frames are page_pool backed.  aq_xdp_xmit() keeps submitting after a
failure and returns a count:

	for (i = 0; i < num_frames; i++) {
		struct xdp_frame *xdpf = frames[i];

		if (aq_nic_xmit_xdpf(aq_nic, ring, xdpf) == NETDEV_TX_BUSY)
			drop++;
	}

	return num_frames - drop;

The core treats the return value as the length of a consumed prefix,
bq_xmit_all() in kernel/bpf/devmap.c:

	/* If not all frames have been transmitted, it is our
	 * responsibility to free them
	 */
	for (i = sent; unlikely(i < to_send); i++)
		xdp_return_frame_rx_napi(bq->q[i]);

and aq_nic_xmit_xdpf() returns NETDEV_TX_BUSY without freeing xdpf when
aq_nic_map_xdp() fails on dma_map_single()/skb_frag_dma_map():

	frags = aq_nic_map_xdp(aq_nic, xdpf, tx_ring);
	if (likely(frags))
		err = aq_nic->aq_hw_ops->hw_ring_tx_xmit(aq_nic->aq_hw, tx_ring,
							 frags);

So if a middle frame fails while a later one is queued successfully, the
core frees a frame that is still sitting in the TX ring, and the frame that
actually failed is never released.

After this patch, can that first frame then be released a second time here
through xdp_return_frame() -> page_pool_put_full_netmem(), underflowing
pp_ref_count and recycling a page the NIC is still DMAing from?  And does
the never-released frame hold a pool fragment forever, so the new
page_pool_destroy() call in aq_ring_free() stalls?

Would returning the length of the successfully transmitted prefix (breaking
out of the loop on the first failure) fix the accounting?

[ ... ]

> @@ -546,8 +489,11 @@ static bool aq_add_rx_fragment(struct device *dev,
>  	do {
>  		skb_frag_t *frag;
>  
> -		if (unlikely(sinfo->nr_frags >= MAX_SKB_FRAGS))
> +		if (unlikely(sinfo->nr_frags >= MAX_SKB_FRAGS)) {
> +			/* Attached frags must reach xdp_return_buff() */
> +			xdp_buff_set_frags_flag(xdp);
>  			return true;
> +		}
>  
>  		frag = &sinfo->frags[sinfo->nr_frags++];
>  		buff_ = &ring->buff_ring[buff_->next];
> @@ -571,6 +517,11 @@ static bool aq_add_rx_fragment(struct device *dev,
>  		if (page_is_pfmemalloc(buff_->rxdata.page))
>  			xdp_buff_set_frag_pfmemalloc(xdp);
>  
> +		/* The frag's page pool reference is owned by the xdp_buff
> +		 * from now on.
> +		 */
> +		buff_->rxdata.page = NULL;
> +
>  	} while (!buff_->is_eop);

[Severity: Low]
This isn't a bug, but the reported validation is:

  "smoke-tested with native XDP PASS, DROP, and ABORTED actions"
  at "MTU 1500"

Those three verdicts are exactly the paths that simply call
xdp_return_buff() in the new code.  The hunks with the ownership transfers
do not appear to be covered:

 - the per-fragment transfer above plus the new xdp_buff_set_frags_flag()
   on the MAX_SKB_FRAGS abort
 - the two buff->rxdata.page = NULL sites in __aq_ring_rx_clean()
 - XDP_TX and XDP_REDIRECT in aq_xdp_run_prog(), which lost their
   aq_get_rxpages_xdp() reference acquisition and now rely entirely on the
   caller having nulled buff->rxdata.page
 - the PTP RX ring, newly registered with MEM_TYPE_PAGE_POOL and sharing
   __aq_ring_xdp_clean()

Multi-buffer XDP cannot be reached at MTU 1500: aq_ring_rx_alloc() sets
self->frame_max = AQ_CFG_RX_FRAME_MAX (2048) and aq_xdp_setup() only allows
a larger MTU for frags-capable programs:

	if (prog && !prog->aux->xdp_has_frags) {
		if (ndev->mtu > AQ_CFG_RX_FRAME_MAX) {

so aq_add_rx_fragment() is never entered in that test.  Could the multi-
buffer, XDP_TX/XDP_REDIRECT and PTP-ring paths be exercised too (for
example at MTU 9000 with an xdp.frags program), so the fragment ownership
transfers are covered by more than inspection?

[ ... ]

> @@ -851,6 +803,11 @@ static int __aq_ring_xdp_clean(struct aq_ring_s *rx_ring,
>  		xdp_init_buff(&xdp, frame_sz, &rx_ring->xdp_rxq);
>  		xdp_prepare_buff(&xdp, hard_start, rx_ring->page_offset,
>  				 buff->len, false);
> +		/* The xdp_buff owns the buffer's page pool reference from
> +		 * here on; it comes back through the MEM_TYPE_PAGE_POOL
> +		 * memory model on every XDP verdict.
> +		 */
> +		buff->rxdata.page = NULL;

[Severity: High]
This is a pre-existing issue and the loops are untouched by the diff, but
since this patch reworks buffer ownership inside this function it may be
worth fixing here: the two descriptor-chain walks earlier in
__aq_ring_xdp_clean() are unbounded.  The RSC completion walk:

			do {
				if (buff_->next >= rx_ring->size) {
					err = -EIO;
					goto err_exit;
				}
				next_ = buff_->next;
				buff_ = &rx_ring->buff_ring[next_];
				is_rsc_completed =
					aq_ring_dx_in_range(rx_ring->sw_head,
							    next_,
							    rx_ring->hw_head);

				if (unlikely(!is_rsc_completed))
					break;
				...
			} while (!buff_->is_eop);

and the error-drain walk right below it follow buff->next, which for LRO
descriptors is copied straight out of the hardware write-back in
hw_atl_b0_hw_ring_rx_receive():

			if (buff->is_lro) {
				/* LRO */
				buff->next = rxd_wb->next_desc_ptr;

Can a cyclic chain A->B->A, where both entries are inside the
[sw_head, hw_head] window and neither has is_eop set, spin here forever
with BH disabled in the NAPI poll?  The non-XDP sibling
__aq_ring_rx_clean() bounds the identical walk:

				if (unlikely(!is_rsc_completed) ||
				    frag_cnt > MAX_SKB_FRAGS) {
					err = 0;
					goto err_exit;
				}

Should the XDP path carry the same hop counter?

>  		if (!buff->is_eop) {
>  			if (aq_add_rx_fragment(dev, rx_ring, buff, &xdp)) {
>  				u64_stats_update_begin(&rx_ring->stats.rx.syncp);

[ ... ]
-- 
pw-bot: cr

      parent reply	other threads:[~2026-08-17 16:59 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-07  8:56 [PATCH net-next v3] net: atlantic: convert RX path to page_pool Yangyu Chen
2026-08-08  8:56 ` sashiko-bot
2026-08-17 16:59 ` Jakub Kicinski [this message]

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=20260817165919.3422774-1-kuba@kernel.org \
    --to=kuba@kernel.org \
    --cc=almasrymina@google.com \
    --cc=andrew+netdev@lunn.ch \
    --cc=bpf@vger.kernel.org \
    --cc=cyy@cyyself.name \
    --cc=davem@davemloft.net \
    --cc=edumazet@google.com \
    --cc=hawk@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=netdev@vger.kernel.org \
    --cc=pabeni@redhat.com \
    --cc=richardcochran@gmail.com \
    --cc=sukhdeeps@marvell.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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.