From: Lorenzo Bianconi <lorenzo@kernel.org>
To: Vitaliy Sochnev <sochnev.v.74@gmail.com>
Cc: netdev@vger.kernel.org, Andrew Lunn <andrew+netdev@lunn.ch>,
"David S . Miller" <davem@davemloft.net>,
Eric Dumazet <edumazet@google.com>,
Jakub Kicinski <kuba@kernel.org>, Paolo Abeni <pabeni@redhat.com>,
linux-mediatek@lists.infradead.org,
linux-arm-kernel@lists.infradead.org,
linux-kernel@vger.kernel.org
Subject: Re: [PATCH net-next 2/4] net: airoha: recover RX ring after hw completion race
Date: Sun, 30 Aug 2026 16:18:37 +0200 [thread overview]
Message-ID: <apQ7vXZHiK5qZOcc@lore-desk> (raw)
In-Reply-To: <20260830095717.37218-3-sochnev.v.74@gmail.com>
[-- Attachment #1: Type: text/plain, Size: 14075 bytes --]
Hi Vitaliy,
this patch targets net-next tree that is closed at the moment for the merge
window.
> On AN7583, hardware can write a completed RX descriptor past the
> software-posted boundary (q->head) before airoha_qdma_fill_rx_queue()
> has actually posted a fresh buffer there, as if hardware advances its
> own completion pointer independently of software's posting
> bookkeeping. Since airoha_qdma_rx_process() consumes the ring strictly
> sequentially starting at q->tail, the consumer stalls forever waiting
> on a descriptor that hardware never writes, even though real,
> completed frames are sitting further along in the ring. In practice
> this is reachable during PPPoE/DHCP negotiation bursts on the small
> shared "force to CPU" ring, and the interface silently stops receiving
> on it.
I guess we should spend more time understanding if we can avoid the problem
instead of adding a recover mechanism.
IIUC the practical problem stems from the small size of this RX queue where
the airoha_qdma_fill_rx_queue() processing overwrites the RX descriptor DONE
bit written by the hw. Is my understanding correct? If so:
- Have you tried to just increase the queue size to check if the issue still
occurs?
- Can we check QDMA_DESC_DONE_MASK bit in airoha_qdma_fill_rx_queue() and in
some way bail out if it is still set? (we should probably need to clear it in
airoha_qdma_rx_process()).
- Is QDMA_DESC_DROP_MASK bit set when the issue occurs?
>
> Detect this without inspecting ring/descriptor memory content at all:
> REG_RX_DMA_IDX is hardware's own completion counter, independent of
> what has or hasn't been posted. If it keeps advancing across polls
> while the software consumer (tail) does not move, hardware is making
> progress the consumer can never observe - the ring is stuck. Idle
> rings, where hardware isn't advancing either, are correctly left
> alone. An earlier version of this recovery instead scanned ahead in
> the ring for a DONE descriptor and trusted its content; that caused a
> real OOM panic once it wandered into genuinely uninitialized DMA
> memory that coincidentally had the DONE bit set. Comparing a hardware
> register cannot misfire that way.
>
> Once a stall is confirmed, defer to a work item (register access here
> can sleep) that disables RX DMA, waits for it to actually go idle,
> resyncs the ring via the existing cleanup_rx_queue()/fill_rx_queue()
> pair - which only ever touches the software-owned [tail, head) window
> and rewrites both RX_CPU_IDX and RX_DMA_IDX from it - and re-enables
> RX DMA. This deliberately drops whatever was in flight on the ring
> rather than trying to identify and preserve the specific descriptor
> hardware used; a prior attempt at the latter caused a page_pool
> double-free when the assumptions about which page was safe to free
> turned out not to hold in all cases.
>
> GLOBAL_CFG_RX_DMA_EN_MASK in REG_QDMA_GLOBAL_CFG is per-QDMA-instance,
> not per-ring, so recovering one ring briefly pauses RX DMA on every
> ring behind that QDMA (bounded by the 50ms busy-wait below). No
> per-ring equivalent exists in the register map; this is the same bit
> airoha_qdma_start()/stop() already use for whole-device up/down. Log
> the actual measured duration of that pause alongside the recovery
> message, rather than just citing the 50ms read_poll_timeout() upper
> bound: that's the real cost paid by every other ring on the same QDMA
> instance each time recovery fires, and it's worth having the real
> number instead of the theoretical ceiling.
We should try to avoid it
>
> One cost worth calling out explicitly: airoha_qdma_rx_check_stall()
> adds an uncached MMIO read of RX_DMA_IDX on the RX path, once per
> airoha_qdma_rx_process() call that ends via the non-DONE break - i.e.
> essentially every poll, on every ring, even though the condition it
> detects is rare and specific to one ring. Gating the read on whether
> the *previous* poll for this ring also reaped zero descriptors would
> keep it off rings that are actively receiving, and I traced that
> through for correctness: once the race actually happens, q->tail
> freezes permanently (consumption here is strictly sequential), so
> `done` is 0 on every poll after that point, not just some - the gate
> would only cost about one extra poll before AIROHA_RX_STALL_THRESHOLD
> is reached, not a suppression or false-negative risk. I haven't
> implemented that gating here for lack of profiling data justifying the
> added per-queue state against the (also unmeasured) cost of the
> current unconditional read; happy to add it if it turns out to matter
> in practice.
>
> Fixes: 23290c7bc190 ("net: airoha: Introduce Airoha NPU support")
> Signed-off-by: Vitaliy Sochnev <sochnev.v.74@gmail.com>
> ---
> drivers/net/ethernet/airoha/airoha_eth.c | 135 ++++++++++++++++++++++-
> drivers/net/ethernet/airoha/airoha_eth.h | 16 +++
> 2 files changed, 150 insertions(+), 1 deletion(-)
>
> diff --git a/drivers/net/ethernet/airoha/airoha_eth.c b/drivers/net/ethernet/airoha/airoha_eth.c
> index a3e5aaeb75b3..b53fe5b17653 100644
> --- a/drivers/net/ethernet/airoha/airoha_eth.c
> +++ b/drivers/net/ethernet/airoha/airoha_eth.c
> @@ -3,12 +3,14 @@
> * Copyright (c) 2024 AIROHA Inc
> * Author: Lorenzo Bianconi <lorenzo@kernel.org>
> */
> +#include <linux/ktime.h>
> #include <linux/of.h>
> #include <linux/of_net.h>
> #include <linux/of_reserved_mem.h>
> #include <linux/platform_device.h>
> #include <linux/tcp.h>
> #include <linux/u64_stats_sync.h>
> +#include <linux/workqueue.h>
> #include <net/dst_metadata.h>
> #include <net/page_pool/helpers.h>
> #include <net/pkt_cls.h>
> @@ -657,6 +659,47 @@ airoha_qdma_get_gdm_dev(struct airoha_eth *eth, struct airoha_qdma_desc *desc)
> return port->devs[d] ? port->devs[d] : ERR_PTR(-ENODEV);
> }
>
> +/* number of consecutive polls where hw completion (RX_DMA_IDX) advances
> + * while the sw consumer (tail) doesn't, before declaring the ring stuck
> + */
> +#define AIROHA_RX_STALL_THRESHOLD 3
> +
> +/* Detect an RX ring where hw's own completion pointer (RX_DMA_IDX) keeps
> + * moving while the sw consumer (q->tail) doesn't - i.e. hw has written
> + * further descriptors somewhere in the ring, but the strictly sequential
> + * consumer can never reach them because the one at q->head, which it is
> + * waiting on, was never marked DONE. This happens when hw writes a
> + * completed descriptor past q->head before airoha_qdma_fill_rx_queue()
> + * has posted a fresh buffer there, decoupled from sw's own posting
> + * bookkeeping.
> + *
> + * Deliberately does not inspect ring/descriptor memory content to detect
> + * this: an earlier version scanned ahead for a DONE descriptor and trusted
> + * its content, which caused a real OOM panic after it wandered into
> + * genuinely uninitialized DMA memory that coincidentally had the DONE bit
> + * set. RX_DMA_IDX is a hw register with a well-defined value regardless of
> + * ring content, so this can't misfire on garbage memory, and idle rings
> + * (no hw progress either) are naturally left alone.
> + */
> +static void airoha_qdma_rx_check_stall(struct airoha_queue *q)
> +{
> + struct airoha_qdma *qdma = q->qdma;
> + int qid = q - &qdma->q_rx[0];
> + u32 dma_idx = airoha_qdma_get(qdma, REG_RX_DMA_IDX(qid),
> + RX_RING_DMA_IDX_MASK);
Can you please respect the RCT code style?
> +
> + if (q->stall_tail == q->tail && dma_idx != q->stall_dma_idx) {
> + if (++q->stall_count >= AIROHA_RX_STALL_THRESHOLD &&
> + !test_and_set_bit(qid, qdma->rx_recover_mask))
> + schedule_work(&qdma->rx_recover_work);
> + } else {
> + q->stall_count = 0;
> + }
> +
> + q->stall_tail = q->tail;
> + q->stall_dma_idx = dma_idx;
> +}
> +
> static int airoha_qdma_rx_process(struct airoha_queue *q, int budget)
> {
> enum dma_data_direction dir = page_pool_get_dma_dir(q->page_pool);
> @@ -675,8 +718,10 @@ static int airoha_qdma_rx_process(struct airoha_queue *q, int budget)
> struct page *page;
>
> desc_ctrl = le32_to_cpu(READ_ONCE(desc->ctrl));
> - if (!(desc_ctrl & QDMA_DESC_DONE_MASK))
> + if (!(desc_ctrl & QDMA_DESC_DONE_MASK)) {
> + airoha_qdma_rx_check_stall(q);
> break;
> + }
>
> dma_rmb();
>
> @@ -894,6 +939,75 @@ static void airoha_qdma_cleanup_rx_queue(struct airoha_queue *q)
> FIELD_PREP(RX_RING_DMA_IDX_MASK, q->tail));
> }
>
> +static void airoha_qdma_rx_recover_work(struct work_struct *work)
> +{
> + struct airoha_qdma *qdma = container_of(work, struct airoha_qdma,
> + rx_recover_work);
> + int qid;
> +
> + for_each_set_bit(qid, qdma->rx_recover_mask, AIROHA_NUM_RX_RING) {
> + struct airoha_queue *q = &qdma->q_rx[qid];
> + ktime_t rx_dma_off_ts;
> + s64 rx_dma_off_us;
> + u32 status;
> +
> + if (!q->ndesc)
> + goto next;
Can this really occurs?
> +
> + napi_disable(&q->napi);
> +
> + /* GLOBAL_CFG_RX_DMA_EN_MASK is per-QDMA, not per-ring, so
> + * this pauses every RX ring on this QDMA instance, not just
> + * the stalled one - track how long for, since that's the
> + * real-world cost of recovery on unrelated rings.
> + */
> + rx_dma_off_ts = ktime_get();
> +
> + airoha_qdma_clear(qdma, REG_QDMA_GLOBAL_CFG,
> + GLOBAL_CFG_RX_DMA_EN_MASK);
> + if (read_poll_timeout(airoha_qdma_rr, status,
> + !(status & GLOBAL_CFG_RX_DMA_BUSY_MASK),
> + USEC_PER_MSEC, 50 * USEC_PER_MSEC, true,
> + qdma, REG_QDMA_GLOBAL_CFG))
> + dev_warn(qdma->eth->dev,
> + "qid=%d RX DMA busy timeout during recovery\n",
> + qid);
> +
> + /* Drop whatever is currently in flight on this ring and
> + * re-arm it from a known-clean state. cleanup_rx_queue()
> + * only ever touches the sw-owned [tail, head) window and
> + * resyncs both RX_CPU_IDX and RX_DMA_IDX to it, which is
> + * what un-wedges a ring where hw wrote past the sw head
> + * without the consumer ever advancing - no need to figure
> + * out which descriptor hw actually used.
> + */
> + airoha_qdma_cleanup_rx_queue(q);
> + if (q->skb) {
> + /* discard whatever scatter-gather frame was
> + * mid-assembly when the stall was hit, cleanup_rx_queue()
> + * above only resyncs the ring, not this
> + */
> + dev_kfree_skb(q->skb);
> + q->skb = NULL;
> + }
> + airoha_qdma_fill_rx_queue(q);
> +
> + airoha_qdma_set(qdma, REG_QDMA_GLOBAL_CFG,
> + GLOBAL_CFG_RX_DMA_EN_MASK);
> + rx_dma_off_us = ktime_us_delta(ktime_get(), rx_dma_off_ts);
Is this delta time really meaningful or is it just for debugging?
> +
> + q->stall_count = 0;
> + napi_enable(&q->napi);
> + napi_schedule(&q->napi);
> +
> + dev_warn_ratelimited(qdma->eth->dev,
> + "qid=%d RX ring recovered after hw stall (RX DMA paused for %lld us on this QDMA instance)\n",
> + qid, rx_dma_off_us);
> +next:
> + clear_bit(qid, qdma->rx_recover_mask);
> + }
> +}
> +
> static int airoha_qdma_init_rx(struct airoha_qdma *qdma)
> {
> int i;
> @@ -1594,6 +1708,8 @@ static void airoha_qdma_cleanup(struct airoha_eth *eth,
> {
> int i;
>
> + cancel_work_sync(&qdma->rx_recover_work);
> +
> if (test_bit(DEV_STATE_INITIALIZED, ð->state)) {
> u32 status;
>
> @@ -1651,6 +1767,15 @@ static int airoha_hw_init(struct platform_device *pdev,
> if (err)
> return err;
>
> + /* INIT_WORK() every instance up front, before any of them can fail
> + * init and jump to the error path below, since that path tears down
> + * every eth->qdma[] slot unconditionally, including ones this loop
> + * never reached.
> + */
> + for (i = 0; i < ARRAY_SIZE(eth->qdma); i++)
> + INIT_WORK(ð->qdma[i].rx_recover_work,
> + airoha_qdma_rx_recover_work);
> +
> for (i = 0; i < ARRAY_SIZE(eth->qdma); i++) {
> err = airoha_qdma_init(pdev, eth, ð->qdma[i]);
> if (err)
> @@ -1699,6 +1824,14 @@ static void airoha_qdma_stop_napi(struct airoha_qdma *qdma)
> {
> int i;
>
> + /* Make sure rx_recover_work is neither running nor able to re-arm
> + * before any napi_disable() below: it also calls napi_disable()/
> + * napi_enable() on q_rx[].napi, and napi_disable() on an
> + * already-disabled NAPI spins in napi_disable_locked() forever,
> + * since only napi_enable() clears the state it waits on.
> + */
I guess we can drop this comment. AI driven?
> + disable_work_sync(&qdma->rx_recover_work);
> +
> for (i = 0; i < ARRAY_SIZE(qdma->q_tx_irq); i++)
> napi_disable(&qdma->q_tx_irq[i].napi);
>
> diff --git a/drivers/net/ethernet/airoha/airoha_eth.h b/drivers/net/ethernet/airoha/airoha_eth.h
> index fa9a8edce22f..483d6b59c351 100644
> --- a/drivers/net/ethernet/airoha/airoha_eth.h
> +++ b/drivers/net/ethernet/airoha/airoha_eth.h
> @@ -207,6 +207,15 @@ struct airoha_queue {
> bool txq_stopped;
> bool flushing;
>
> + /* RX hw stall detection: last REG_RX_DMA_IDX/tail snapshot taken
> + * whenever the head-of-line descriptor isn't DONE, and how many
> + * consecutive times hw made progress (DMA_IDX moved) while the
> + * consumer (tail) didn't. See airoha_qdma_rx_check_stall().
> + */
> + u32 stall_dma_idx;
> + u16 stall_tail;
> + u8 stall_count;
> +
> struct napi_struct napi;
> struct page_pool *page_pool;
> struct sk_buff *skb;
> @@ -567,6 +576,13 @@ struct airoha_qdma {
> struct airoha_queue q_tx[AIROHA_NUM_TX_RING];
> struct airoha_queue q_rx[AIROHA_NUM_RX_RING];
>
> + /* recovery for RX rings whose hw completion pointer (RX_DMA_IDX)
> + * keeps moving while the sw consumer is stuck; see
> + * airoha_qdma_rx_check_stall() and airoha_qdma_rx_recover_work().
> + */
> + struct work_struct rx_recover_work;
> + DECLARE_BITMAP(rx_recover_mask, AIROHA_NUM_RX_RING);
> +
> DECLARE_BITMAP(qos_channel_map, AIROHA_NUM_QOS_CHANNELS);
> };
>
> --
> 2.55.0
>
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 228 bytes --]
next prev parent reply other threads:[~2026-08-30 14:18 UTC|newest]
Thread overview: 23+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-30 9:57 [PATCH 0/4] net: airoha: fix silent RX packet loss on ring 4 Vitaliy Sochnev
2026-08-30 9:57 ` [PATCH net 1/4] net: airoha: handle RX_NO_CPU_DSCP interrupt, not just RX_DONE Vitaliy Sochnev
2026-08-30 13:26 ` Lorenzo Bianconi
2026-08-30 9:57 ` [PATCH net-next 2/4] net: airoha: recover RX ring after hw completion race Vitaliy Sochnev
2026-08-30 14:18 ` Lorenzo Bianconi [this message]
2026-08-30 9:57 ` [PATCH net-next 3/4] net: airoha: add rx_stall_recover ethtool counter Vitaliy Sochnev
2026-08-30 9:57 ` [PATCH net-next 4/4] net: airoha: grow RX ring 4 to 128 descriptors Vitaliy Sochnev
2026-08-30 14:24 ` Lorenzo Bianconi
2026-08-31 23:46 ` [PATCH net v2 0/3] net: airoha: fix silent RX loss on the shared CPU ring Vitaliy Sochnev
2026-08-31 23:46 ` [PATCH net v2 1/3] net: airoha: handle RX_NO_CPU_DSCP interrupt, not just RX_DONE Vitaliy Sochnev
2026-08-31 23:47 ` [PATCH net v2 2/3] net: airoha: recover RX ring after hw completion stall Vitaliy Sochnev
2026-09-01 7:38 ` Lorenzo Bianconi
2026-09-01 18:33 ` Vitaliy Sochnev
2026-08-31 23:47 ` [PATCH net v2 3/3] net: airoha: grow the small RX rings Vitaliy Sochnev
2026-09-01 7:23 ` Lorenzo Bianconi
2026-09-01 18:32 ` [PATCH net v3 0/2] net: airoha: fix silent RX loss on the shared CPU ring Vitaliy Sochnev
2026-09-01 18:32 ` [PATCH net v3 1/2] net: airoha: handle RX_NO_CPU_DSCP interrupt, not just RX_DONE Vitaliy Sochnev
2026-09-04 0:35 ` Jakub Kicinski
2026-09-10 23:06 ` Vitaliy Sochnev
2026-09-04 0:50 ` patchwork-bot+netdevbpf
2026-09-01 18:32 ` [PATCH net v3 2/2] net: airoha: grow the small RX rings Vitaliy Sochnev
2026-09-06 6:37 ` net: airoha: RX rings below 32 descriptors let hw DMA past the ring Vitaliy Sochnev
2026-09-06 8:02 ` Vitaliy Sochnev
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=apQ7vXZHiK5qZOcc@lore-desk \
--to=lorenzo@kernel.org \
--cc=andrew+netdev@lunn.ch \
--cc=davem@davemloft.net \
--cc=edumazet@google.com \
--cc=kuba@kernel.org \
--cc=linux-arm-kernel@lists.infradead.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-mediatek@lists.infradead.org \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
--cc=sochnev.v.74@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 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.