Re: [PATCH] wifi: brcmfmac: avoid sleeping tx locks in netpoll context
From: Arend van Spriel
Date: Sun Oct 04 2026 - 08:29:41 EST
On Wed, 30 Sep 2026 20:54:17 +0200, Karl Mehltretter wrote:
> With the default fcmode=0, netpoll calls ndo_start_xmit() with hard
> interrupts disabled and reaches brcmf_sdio_bus_txdata() directly. The
> function takes txq_lock with spin_lock_bh(), and the queue helper takes
> the embedded sk_buff_head lock. These locks may sleep on PREEMPT_RT.
> On non-RT, spin_unlock_bh() can run pending networking softirqs before
> netpoll releases the transmit lock, causing a recursive transmit
> deadlock.
>
> Use spin_trylock() for IRQ-disabled calls and enqueue with the unlocked
> skb helper while holding txq_lock. On PREEMPT_RT, reject hard IRQ and NMI
> callers, where rt-spinlocks cannot be acquired. If the lock is busy or
> the queue is full, return through the existing drop path. Do not evict an
> older packet from this context. Suppress the queue-full printk because
> netconsole can recursively enter this path.
>
> The flow-control callback takes another spinlock, so defer it when an
> IRQ-disabled enqueue reaches TXHI. The data worker rechecks the bus state
> and queue length under txq_lock before stopping the queue. Existing TXLOW
> handling wakes the queue after it drains.
>
> This fixes the direct SDIO transmit path used by fcmode=0. Modes 1 and 2
> take the FWS lock first and need a separate change.
>
> Fixes: ac3d9dd034e5 ("netpoll: make ndo_poll_controller() optional")
> Cc: stable@xxxxxxxxxxxxxxx
> Assisted-by: LLM
> Signed-off-by: Karl Mehltretter <kmehltretter@xxxxxxxxx>
[...]
> @@ -2796,8 +2797,53 @@ static bool brcmf_sdio_prec_enq(struct pktq *q, struct sk_buff *pkt, int prec)
> return p != NULL;
> }
>
> +/*
> + * The caller holds txq_lock with hard IRQs disabled. Avoid the skb queue
> + * lock, which may sleep on PREEMPT_RT.
> + */
> +static bool brcmf_sdio_prec_enq_irqoff(struct pktq *q, struct sk_buff *pkt,
> + int prec)
> +{
> + struct sk_buff_head *list = &q->q[prec].skblist;
> +
> + if (pktq_pfull(q, prec) || pktq_full(q))
> + return false;
> +
> + __skb_queue_tail(list, pkt);
> + q->len++;
> + if (q->hi_prec < prec)
> + q->hi_prec = prec;
> +
> + return true;
> +}
Looking at brcmf_sdio_prec_enq(), it performs essentially the same check on
its fast path (!pktq_pfull(q, prec) && !pktq_full(q)) before calling
brcmu_pktq_penq(), and only falls back to packet eviction when the queue is
full.
Since bus->txq_lock is always held by the caller, using the unlocked
__skb_queue_tail() is safe in both contexts (the embedded skblist spinlock
in brcmu_pktq_penq() has always been redundant in sdio.c).
Is there an opportunity to reuse/refactor brcmf_sdio_prec_enq() rather than
adding a separate brcmf_sdio_prec_enq_irqoff() helper? For example, passing
a can_evict parameter:
static bool brcmf_sdio_prec_enq(struct pktq *q, struct sk_buff *pkt, int prec,
bool can_evict)
{
struct sk_buff_head *list = &q->q[prec].skblist;
struct sk_buff *p;
int eprec = -1;
/* Fast case, precedence queue is not full and we are also not
* exceeding total queue length
*/
if (!pktq_pfull(q, prec) && !pktq_full(q))
goto enq;
if (!can_evict)
return false;
/* Determine precedence from which to evict packet, if any */
if (pktq_pfull(q, prec)) {
eprec = prec;
} else if (pktq_full(q)) {
p = brcmu_pktq_peek_tail(q, &eprec);
if (eprec > prec)
return false;
}
/* Evict if needed */
if (eprec >= 0) {
/* Detect queueing to unconfigured precedence */
if (eprec == prec)
return false; /* refuse newer (incoming) packet */
/* Evict packet according to discard policy */
p = brcmu_pktq_pdeq_tail(q, eprec);
if (p == NULL)
brcmf_err("brcmu_pktq_pdeq_tail() failed\n");
brcmu_pkt_buf_free_skb(p);
}
enq:
__skb_queue_tail(list, pkt);
q->len++;
if (q->hi_prec < prec)
q->hi_prec = prec;
return true;
}
Then in brcmf_sdio_bus_txdata():
if (!brcmf_sdio_prec_enq(&bus->txq, pkt, prec, !irq_off)) {
skb_pull(pkt, bus->tx_hdrlen);
if (!irq_off)
brcmf_err("out of bus->txq !!!\n");
ret = -ENOSR;
} else {
ret = 0;
}
That keeps the queuing logic in one place, eliminates the duplicate helper,
and simplifies the caller.
Regards,
Arend