* [PATCH net] gve: leave refcount headroom in the RX page bias
@ 2026-10-31 17:34 Amaan Cheval
2026-11-01 18:19 ` netdev-bot+sashiko
0 siblings, 1 reply; 2+ messages in thread
From: Amaan Cheval @ 2026-10-31 17:34 UTC (permalink / raw)
To: netdev
Cc: linux-kernel, Amaan Cheval, stable, Joshua Washington,
Harshitha Ramamurthy, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Zhiling Zou, Jiayuan Chen,
Kuniyuki Iwashima
gve's GQI and DQO-QPL RX paths set every RX page's refcount to INT_MAX and
hand references to the stack by decrementing page_info->pagecnt_bias, to
avoid an atomic op per packet. This leaves the page no headroom: while a
fresh page's buffer sits in an skb its refcount is already INT_MAX, so a
single get_page() from the stack wraps it negative.
splice() of received TCP data into a pipe takes such a reference, and the
onward splice into a socket then fails: skb_splice_from_iter() sees
page_count() < 1, sendpage_ok() fails and the send returns -EIO (since
commit 2e910b95329c ("net: Add a function to splice pages into an
skbuff for MSG_SPLICE_PAGES"))
tee() of the pipe fails with -EFAULT via try_get_page(). On GQI the reuse
check gve_rx_can_recycle_buffer() also sees page_count() < pagecnt_bias,
WARNs and calls gve_schedule_reset(), so a task that merely pins a fresh
RX page (e.g. holds spliced data in an undrained tee() pipe) can reset
the NIC.
Use USHRT_MAX in a u16, as igb, igc, ixgbe, i40e and hns3 do: the bias only
has to make the per-packet atomic rare so a small value leaves page_count()
ample headroom below INT_MAX for the stack's references, and the u16 bounds
it. Refill by adding a fixed batch and setting the bias to it, which keeps
the rule `references = page_count() - bias` intact.
Reproduced on gVNIC in GQI-QPL mode (GCE n2-standard-8, 6.12), no kernel
changes needed, with relay.py -- a small splice(2) TCP proxy -- relaying a
download while the RX ring is refilled with fresh pages:
ethtool -G eth0 rx 512 && ethtool -G eth0 rx 1024 # fresh RX pages
./relay.py $HOST 8080 & # splice()s both ways
for i in $(seq 200); do curl -so /dev/null \
--connect-to $HOST:80:127.0.0.1:8080 http://$HOST/file; done
Most downloads failed with EIO before the fix (tee() with EFAULT) and all
completed after it, including with the bias forced to 1. Built allmodconfig
W=1, no new warnings; no selftests run. relay.py is at the Link below.
This bug was found and fixed with the help of an AI coding assistant
(Claude Code) over an interactive session; it read the driver and
splice/pipe code and ran the tests above.
Link: https://github.com/amaanc/kernel-bias
Fixes: 9b8dd5e5ea48 ("gve: DQO: Add RX path")
Fixes: 58401b2a46e7 ("gve: Add rx buffer pagecnt bias")
Fixes: 82fd151d38d9 ("gve: Reduce alloc and copy costs in the GQ rx path")
Cc: stable@vger.kernel.org
Signed-off-by: Amaan Cheval <amaan.cheval@gmail.com>
---
drivers/net/ethernet/google/gve/gve.h | 2 +-
.../net/ethernet/google/gve/gve_buffer_mgmt_dqo.c | 4 ++--
drivers/net/ethernet/google/gve/gve_rx.c | 8 ++++----
drivers/net/ethernet/google/gve/gve_utils.c | 12 +++---------
drivers/net/ethernet/google/gve/gve_utils.h | 8 +++++++-
5 files changed, 17 insertions(+), 17 deletions(-)
diff --git a/drivers/net/ethernet/google/gve/gve.h b/drivers/net/ethernet/google/gve/gve.h
index b7ce6e8632db..18d7edbf4b3b 100644
--- a/drivers/net/ethernet/google/gve/gve.h
+++ b/drivers/net/ethernet/google/gve/gve.h
@@ -119,7 +119,7 @@ struct gve_rx_slot_page_info {
void *page_address;
u32 page_offset; /* offset to write to in page */
unsigned int buf_size;
- int pagecnt_bias; /* expected pagecnt if only the driver has a ref */
+ u16 pagecnt_bias; /* expected pagecnt if only the driver has a ref */
u16 pad; /* adjustment for rx padding */
u8 can_flip; /* tracks if the networking stack is using the page */
};
diff --git a/drivers/net/ethernet/google/gve/gve_buffer_mgmt_dqo.c b/drivers/net/ethernet/google/gve/gve_buffer_mgmt_dqo.c
index 6880d1531ba5..571f851d6dcd 100644
--- a/drivers/net/ethernet/google/gve/gve_buffer_mgmt_dqo.c
+++ b/drivers/net/ethernet/google/gve/gve_buffer_mgmt_dqo.c
@@ -149,8 +149,8 @@ int gve_alloc_qpl_page_dqo(struct gve_rx_ring *rx,
buf_state->last_single_ref_offset = 0;
/* The page already has 1 ref. */
- page_ref_add(buf_state->page_info.page, INT_MAX - 1);
- buf_state->page_info.pagecnt_bias = INT_MAX;
+ page_ref_add(buf_state->page_info.page, GVE_PAGECNT_BIAS - 1);
+ buf_state->page_info.pagecnt_bias = GVE_PAGECNT_BIAS;
return 0;
}
diff --git a/drivers/net/ethernet/google/gve/gve_rx.c b/drivers/net/ethernet/google/gve/gve_rx.c
index 81ea800e66e9..6533fff06155 100644
--- a/drivers/net/ethernet/google/gve/gve_rx.c
+++ b/drivers/net/ethernet/google/gve/gve_rx.c
@@ -152,8 +152,8 @@ static void gve_setup_rx_buffer(struct gve_rx_ring *rx,
page_info->buf_size = rx->packet_buffer_size;
*slot_addr = cpu_to_be64(addr);
/* The page already has 1 ref */
- page_ref_add(page, INT_MAX - 1);
- page_info->pagecnt_bias = INT_MAX;
+ page_ref_add(page, GVE_PAGECNT_BIAS - 1);
+ page_info->pagecnt_bias = GVE_PAGECNT_BIAS;
}
static int gve_rx_alloc_buffer(struct gve_priv *priv, struct device *dev,
@@ -230,8 +230,8 @@ static int gve_rx_prefill_pages(struct gve_rx_ring *rx,
rx->qpl_copy_pool[j].buf_size = rx->packet_buffer_size;
/* The page already has 1 ref. */
- page_ref_add(page, INT_MAX - 1);
- rx->qpl_copy_pool[j].pagecnt_bias = INT_MAX;
+ page_ref_add(page, GVE_PAGECNT_BIAS - 1);
+ rx->qpl_copy_pool[j].pagecnt_bias = GVE_PAGECNT_BIAS;
}
}
diff --git a/drivers/net/ethernet/google/gve/gve_utils.c b/drivers/net/ethernet/google/gve/gve_utils.c
index caed38cd5f2b..cb6fba3122d5 100644
--- a/drivers/net/ethernet/google/gve/gve_utils.c
+++ b/drivers/net/ethernet/google/gve/gve_utils.c
@@ -93,15 +93,9 @@ void gve_dec_pagecnt_bias(struct gve_rx_slot_page_info *page_info)
{
page_info->pagecnt_bias--;
if (page_info->pagecnt_bias == 0) {
- int pagecount = page_count(page_info->page);
-
- /* If we have run out of bias - set it back up to INT_MAX
- * minus the existing refs.
- */
- page_info->pagecnt_bias = INT_MAX - pagecount;
-
- /* Set pagecount back up to max. */
- page_ref_add(page_info->page, INT_MAX - pagecount);
+ /* Out of bias: take another batch of references. */
+ page_info->pagecnt_bias = GVE_PAGECNT_BIAS;
+ page_ref_add(page_info->page, GVE_PAGECNT_BIAS);
}
}
diff --git a/drivers/net/ethernet/google/gve/gve_utils.h b/drivers/net/ethernet/google/gve/gve_utils.h
index bf2e9a0adb36..316f97607351 100644
--- a/drivers/net/ethernet/google/gve/gve_utils.h
+++ b/drivers/net/ethernet/google/gve/gve_utils.h
@@ -25,7 +25,13 @@ struct sk_buff *gve_rx_copy_data(struct net_device *dev, struct napi_struct *nap
struct sk_buff *gve_rx_copy(struct net_device *dev, struct napi_struct *napi,
struct gve_rx_slot_page_info *page_info, u16 len);
-/* Decrement pagecnt_bias. Set it back to INT_MAX if it reached zero. */
+/* Up-front references gve holds on an RX page, so handing a buffer to the
+ * stack is a pagecnt_bias decrement, not a get_page(). Keep it in a u16 so
+ * the 32-bit page refcount keeps the headroom every page needs below INT_MAX.
+ */
+#define GVE_PAGECNT_BIAS USHRT_MAX
+
+/* Decrement pagecnt_bias. Top it up by GVE_PAGECNT_BIAS if it reached zero. */
void gve_dec_pagecnt_bias(struct gve_rx_slot_page_info *page_info);
void gve_add_napi(struct gve_priv *priv, int ntfy_idx,
base-commit: d8a4f4c59da52b7c31ae337243b8473a19a3f819
--
2.47.3
^ permalink raw reply related [flat|nested] 2+ messages in thread
* Re: [PATCH net] gve: leave refcount headroom in the RX page bias
2026-10-31 17:34 [PATCH net] gve: leave refcount headroom in the RX page bias Amaan Cheval
@ 2026-11-01 18:19 ` netdev-bot+sashiko
0 siblings, 0 replies; 2+ messages in thread
From: netdev-bot+sashiko @ 2026-11-01 18:19 UTC (permalink / raw)
To: amaan.cheval
Cc: netdev, linux-kernel, stable, joshwash, hramamurthy,
andrew+netdev, davem, edumazet, kuba, pabeni, zhilinz,
jiayuan.chen, kuniyu
Thank you for your contribution! Sashiko AI review found 1 potential
issue(s) to consider.
Critical: 1 · High: 0 · Medium: 0 (1) · Low: 0
If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).
- [Critical] This patch moves gve's RX page bias from INT_MAX to
USHRT_MAX, but keeps the old refill rule in gve_dec_pagecnt_bias()…
Pre-existing issues:
- [Medium] This problem wasn't introduced by this patch, but in
gve_rx_copy_to_pool() (drivers/net/ethernet/google/gve/gve_rx.c) the…
--
gve: leave refcount headroom in the RX page bias
This patch changes the GQI and DQO-QPL RX page bias from INT_MAX to
USHRT_MAX and stores pagecnt_bias in a u16. A get_page() from the stack
therefore no longer wraps the page refcount negative.
gve_dec_pagecnt_bias() now adds a fixed batch of GVE_PAGECNT_BIAS
references when the bias reaches zero.
Link: https://github.com/amaanc/kernel-bias
> diff --git a/drivers/net/ethernet/google/gve/gve_rx.c b/drivers/net/ethernet/google/gve/gve_rx.c
> index 81ea800e66e94..6533fff061555 100644
> --- a/drivers/net/ethernet/google/gve/gve_rx.c
> +++ b/drivers/net/ethernet/google/gve/gve_rx.c
> @@ -152,8 +152,8 @@ static void gve_setup_rx_buffer(struct gve_rx_ring *rx,
> page_info->buf_size = rx->packet_buffer_size;
> *slot_addr = cpu_to_be64(addr);
> /* The page already has 1 ref */
> - page_ref_add(page, INT_MAX - 1);
> - page_info->pagecnt_bias = INT_MAX;
> + page_ref_add(page, GVE_PAGECNT_BIAS - 1);
> + page_info->pagecnt_bias = GVE_PAGECNT_BIAS;
> }
[Severity: Medium]
This is a pre-existing issue and was not introduced by this patch. While
looking at the GQI-QPL copy pool, does gve_rx_copy_to_pool() leak the
fallback page?
drivers/net/ethernet/google/gve/gve_rx.c:gve_rx_copy_to_pool() {
...
page = alloc_page(GFP_ATOMIC);
if (!page)
return NULL;
alloc_page_info.page = page;
...
skb = gve_rx_add_frags(napi, &alloc_page_info,
PAGE_SIZE,
len, ctx);
...
return skb;
...
}
gve_rx_add_frags() can return NULL before skb_add_rx_frag() attaches
the page:
skb = napi_get_frags(napi);
if (unlikely(!skb))
return NULL;
...
skb = napi_alloc_skb(napi, 0);
if (!skb)
return NULL;
In that case the only pointer to the page is in the stack-local
alloc_page_info. gve_rx_copy_to_pool() then returns NULL without calling
put_page(page).
[ ... ]
> diff --git a/drivers/net/ethernet/google/gve/gve_utils.c b/drivers/net/ethernet/google/gve/gve_utils.c
> index caed38cd5f2b3..cb6fba3122d5b 100644
> --- a/drivers/net/ethernet/google/gve/gve_utils.c
> +++ b/drivers/net/ethernet/google/gve/gve_utils.c
> @@ -93,15 +93,9 @@ void gve_dec_pagecnt_bias(struct gve_rx_slot_page_info *page_info)
> {
> page_info->pagecnt_bias--;
> if (page_info->pagecnt_bias == 0) {
> - int pagecount = page_count(page_info->page);
> -
> - /* If we have run out of bias - set it back up to INT_MAX
> - * minus the existing refs.
> - */
> - page_info->pagecnt_bias = INT_MAX - pagecount;
> -
> - /* Set pagecount back up to max. */
> - page_ref_add(page_info->page, INT_MAX - pagecount);
> + /* Out of bias: take another batch of references. */
> + page_info->pagecnt_bias = GVE_PAGECNT_BIAS;
> + page_ref_add(page_info->page, GVE_PAGECNT_BIAS);
> }
> }
[Severity: Critical]
Can this page_ref_add() run on a QPL page that has already been freed?
The top-up only happens once the bias has reached zero, which is after
the last driver-held reference was handed off. igb, one of the drivers
the commit message cites, refills while the bias is still 1, before the
handoff:
drivers/net/ethernet/intel/igb/igb_main.c:igb_can_reuse_rx_page() {
...
if (unlikely(pagecnt_bias == 1)) {
page_ref_add(page, USHRT_MAX - 1);
rx_buffer->pagecnt_bias = USHRT_MAX;
}
...
}
On DQO-QPL, gve_xdp_done_dqo() calls xdp_do_redirect() first and only
then drops the bias:
case XDP_REDIRECT:
err = xdp_do_redirect(priv->dev, xdp, xprog);
if (unlikely(err))
goto err;
gve_reuse_buffer(rx, buf_state);
gve_reg_xdp_info() registers MEM_TYPE_PAGE_SHARED for QPL queues. With
an AF_XDP socket in copy mode, the redirect target returns the buffer
before xdp_do_redirect() comes back to the driver:
gve_xdp_done_dqo()
xdp_do_redirect()
__xsk_map_redirect()
xsk_rcv()
xdp_return_buff() /* MEM_TYPE_PAGE_SHARED */
page_frag_free()
put_page_testzero() -> free_frozen_pages()
gve_reuse_buffer()
gve_dec_pagecnt_bias()
page_ref_add() /* page already freed */
Each redirect lowers page_count and pagecnt_bias by one together. After
65534 redirects from one QPL page, both are 1, and the remaining
reference is the QPL's own base reference. The next redirect frees the
page in page_frag_free(), even though the page is still registered with
the NIC and still listed in qpl->pages[].
gve_dec_pagecnt_bias() then sets the bias to 65535 and adds references
to the freed page. The device keeps DMA-ing received packets into it,
and the driver keeps handing it to XDP and to skbs.
Nothing appears to block this setup. gve_verify_xdp_configuration() does
not reject DQO-QPL, and dev_xdp_attach() does not check xdp_features.
The same ordering problem existed with INT_MAX, but then it took about
2^31 handoffs per page. With USHRT_MAX it takes 65535, which sustained
traffic can reach quickly.
Should the refill happen at pagecnt_bias == 1, before the buffer is
handed off, as igb does?
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261031173449.2439047-1-amaan.cheval%40gmail.com
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-11-01 18:19 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-31 17:34 [PATCH net] gve: leave refcount headroom in the RX page bias Amaan Cheval
2026-11-01 18:19 ` netdev-bot+sashiko
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox