Re: [PATCH] wifi: rtw88: sdio: Fix unhandled RX request interrupt storm

From: Alastair D'Silva

Date: Tue Sep 29 2026 - 00:13:57 EST


On Tue, 2026-09-22 at 01:25 +0000, Ping-Ke Shih wrote:
> Alastair D'Silva <alastair@xxxxxxxxxxx> wrote:
>  
> > On Mon, 2026-09-21 at 02:44 +0000, Ping-Ke Shih wrote:
> > > Alastair D'Silva <alastair@xxxxxxxxxxx> wrote:
> > > > 5. Effect on future interrupts:
> > > > -------------------------------
> > > >
> > > > Regarding the internal expert's concern that clearing the bit prevents
> > > > future interrupts: in our testing, writing 1 to clear
> > > > REG_SDIO_HISR_RX_REQUEST after the FIFO is drained did NOT prevent
> > > > subsequent RX interrupts. When new packets arrived over the air, the
> > > > hardware asserted REG_SDIO_HISR_RX_REQUEST again normally.
> > > >
> > > > If there is concern about edge cases (such as hitting the 64KB
> > > > total_rx_bytes limit before the FIFO is completely empty), would it be
> > > > acceptable to only clear REG_SDIO_HISR_RX_REQUEST if
> > > > REG_SDIO_RX0_REQ_LEN reads 0, or re-read REG_SDIO_HISR at the end of
> > > > rtw_sdio_rx_isr()?
> > >
> > > I guess there is a racing between W1C REG_SDIO_HISR_RX_REQUEST and
> > > REG_SDIO_RX0_REQ_LEN == 0.
> > >
> > > With a suggestion from internal, if we want to disable the RX request,
> > > the better way is to disable/enable it by IMR. The corresponding
> > > functions are:
> > >
> > >    rtw_sdio_enable_interrupt()
> > >    rtw_sdio_disable_interrupt()
> > >
> > > To avoid interrupt storm, I personally suggest to combine NAPI, which
> > > disable interrupt when it processes RX budget (I think we can W1C
> > > REG_SDIO_HISR_RX_REQUEST by the way). If (RX) budget is full, it can
> > > poll again by estimated time. Until budget is not full, it re-enable
> > > interrupt.
> > >
> > > Ping-Ke
> >
> > Thanks for the feedback.
> >
> > Regarding using NAPI and IMR: While NAPI and IMR masking is the standard approach for PCIe,
> > implementing true NAPI for the SDIO interface is problematic.
>
> As I know, NAPI is a pure software mechanism, and should not depend on interfaces.
>
> Quickly search for the terms 'napi' and 'sdio' in wireless drivers:
>
> $ git grep napi drivers/net/wireless/ | grep sdio
> drivers/net/wireless/ath/ath10k/sdio.c:         napi_schedule(&ar->napi);
>
> At least ath10k does.
>

While NAPI is a software scheduling mechanism, what can be executed inside
the napi->poll() callback is strictly governed by the execution context.
We audited the entire wireless tree, and ath10k is indeed the only SDIO
driver that registers a NAPI instance. However, ath10k does NOT access the
SDIO bus from within NAPI.

In ath10k (ath10k/sdio.c), the SDIO IRQ handler and a dedicated workqueue
(async_work_rx) synchronously read the hardware mailbox over SDIO and buffer
skbs into an internal queue (rx_head). Only after packets are already in host
memory does it call napi_schedule(), and its napi_poll callback merely
dequeues those buffered skbs and passes them to mac80211.

No other SDIO drivers in the kernel (brcmfmac, mwifiex, wilc1000, rsi, cw1200,
or Realtek's staging rtl8723bs) use NAPI for SDIO. In fact, mt76 explicitly
checks `if (mt76_is_sdio(mdev))` to bypass NAPI entirely and dispatch to a
worker thread. Replicating ath10k's architecture in rtw88 would require adding
intermediate RX queues, an asynchronous worker thread, and flow-control
plumbing, which adds significant complexity to what is otherwise a lean SDIO HCI.

> > napi_poll runs in NET_RX_SOFTIRQ
> > context (which cannot sleep), but reading from the SDIO bus requires sdio_claim_host(), which
> > takes
> > a mutex and must be able to sleep.
>
>
> How about setting NAPI to threaded mode?

Even with threaded NAPI enabled (netif_threaded_enable()), napi->poll()
cannot sleep.

In net/core/dev.c:napi_threaded_poll_loop() (line 7900), the kernel explicitly
disables bottom halves around the poll execution:
local_bh_disable();
__napi_poll(napi, &repoll);
local_bh_enable();

Because bottom halves are disabled, napi->poll() runs in atomic context. Any
attempt to perform SDIO operations will trigger kernel panics:
1. sdio_claim_host() calls __mmc_claim_host(), which has an explicit
might_sleep() and calls schedule() if the host is contended (e.g. by TX).
2. CMD52/CMD53 transfers submit MMC requests and wait on completions
(wait_for_completion()), which sleep.

Calling SDIO functions inside napi->poll(), even in threaded mode, triggers:
"BUG: scheduling while atomic: napi/wlan0-0/.../0x00000101".

Furthermore, the MMC core already provides a dedicated kernel thread for SDIO
IRQs: ksdioirqd/<host> (drivers/mmc/core/sdio_irq.c). When an SDIO interrupt
fires, ksdioirqd wakes up in true, sleepable process context, claims the host
on the driver's behalf, and invokes rtw_sdio_handle_interrupt(). Recreating a
kthread polling mechanism on top of NAPI would effectively duplicate what
ksdioirqd is already doing.

>
> >
> > Fortunately, the kernel's MMC core already runs sdio_irq_thread in process context specifically
> > to
> > handle this. If we simply leave the REG_SDIO_HISR_RX_REQUEST bit asserted in hardware (by
> > conditionally skipping the W1C), the sdio_irq_thread acts exactly like a NAPI polling loop. It
> > will
> > immediately re-invoke our handler in the next cycle, yielding to the scheduler as needed, but
> > safely
> > in process context.
> >
> > This achieves the budget-limited polling you suggested, but avoids the heavy overhead of extra
> > SDIO
> > bus transactions to toggle the IMR on and off.
> >
> >
> > Before I spin a V2 and run through my testing, can you please confirm this aligns with what you
> > were
> > expecting?
>
> There is a racing between reading rx_len (REG_SDIO_RX0_REQ_LEN) and
> writing hisr.
>
>                    rx_len         REG_SDIO_RX0_REQ_LEN      hisr
> T1: read reg          0                     0               hisr
> T2: func return       0                     5               hisr
> T3: W1C               0                     5               &=REG_SDIO_HISR_RX_REQUEST
>
> This is the info I got from internal consulter. You can do real
> experiments (add a long delay after reading register when rx_len == 0)
> to verify if this is correct.
>
> Ping-Ke

You and your internal consultant are completely right about this race window.
If a packet arrives between reading rx_len == 0 and performing the W1C on HISR,
clearing REG_SDIO_HISR_RX_REQUEST will leave the packet stranded in the FIFO
without an asserted interrupt line.

Instead of overhauling the driver for NAPI, this race condition can be solved
cleanly and robustly using the IMR masking suggestion from your internal team:
In rtw_sdio_handle_interrupt():
hisr = rtw_read32(rtwdev, REG_SDIO_HISR);
if (!hisr)
return;
/* Mask interrupts and acknowledge pending status bits */
rtw_sdio_disable_interrupt(rtwdev);
rtw_write32(rtwdev, REG_SDIO_HISR, hisr);
if (hisr & REG_SDIO_HISR_TXERR)
rtw_sdio_tx_err_isr(rtwdev);
if (hisr & REG_SDIO_HISR_RX_REQUEST)
rtw_sdio_rx_isr(rtwdev);
/* Re-enable interrupts: if new packets arrived during rx_isr,
* REG_SDIO_HISR_RX_REQUEST was asserted in hardware; unmasking HIMR
* immediately asserts SDIO DAT[1], causing ksdioirqd to run another pass.
*/
rtw_sdio_enable_interrupt(rtwdev);

I prefer this approach as:
1. It completely closes the race window: any packet arriving during FIFO drainage
latches REG_SDIO_HISR_RX_REQUEST in hardware. When rtw_sdio_enable_interrupt()
restores HIMR, the interrupt line is asserted and ksdioirqd immediately
services the new packet.
2. The overhead of toggling HIMR is just two 4-byte CMD52/CMD53 writes
(~1-2 microseconds total per interrupt burst), which is negligible (<0.1%)
compared to transferring payload data over SDIO.
3. It uses the existing rtw_sdio_disable_interrupt() and rtw_sdio_enable_interrupt()
helpers and requires only ~4 lines of code changes without touching rx_isr.

Alternatively, if we prefer not to touch HIMR, we can perform a drain-and-recheck
inside rtw_sdio_rx_isr():
when rx_len reads 0, issue W1C to REG_SDIO_HISR_RX_REQUEST and immediately re-read
REG_SDIO_RX0_REQ_LEN.
If it reads > 0, continue draining, otherwise break.

I'm not a fan of this approach though, it feels clunky and requires the next buffer
round to be completely drained, resulting in the code consuming more than the 64kb
budget it was allowed.

Toggling HIMR as outlined above is the cleanest and most standard pattern.
Please let me know if this approach is acceptable to you, and I will prepare
and submit v2.

--
Alastair D'Silva