Skip to content

wifi: brcmfmac: close races between bus reset/detach and the IRQ thread - #449

Open
netmojo wants to merge 4 commits into
omacom:integration/aurorafrom
netmojo:fix/brcmfmac-detach-races
Open

netmojo wants to merge 4 commits into
omacom:integration/aurorafrom
netmojo:fix/brcmfmac-detach-races

Conversation

@netmojo

@netmojo netmojo commented Nov 11, 2026 •

Copy link
Copy Markdown

This series closes four races in brcmfmac teardown and attach that can crash the kernel or use freed memory. They happen when brcmf_detach() runs while other code still uses what it frees (on a bus reset after a firmware crash, on unbind, and on modprobe -r), and when the IRQ thread reaches the protocol layer before brcmf_attach() has created it.

Changes

  1. pcie: quiesce the IRQ thread before reset detaches.
    • Race: brcmf_pcie_reset() detaches with the PCIe state still UP, so a running handler can queue a firmware event while fweh is freed, then unmask the interrupt.
    • Fix: reset now does what remove does: set the state DOWN, mask, synchronize the IRQ, and mask again.
  2. Keep fweh until P2P creators are done with it.
    • Race: a P2P_DEVICE create waits for IF_ADD under the wiphy lock, then writes to drvr->fweh. A reset that frees fweh without that lock leaves a timed-out create writing to freed memory.
    • Fix: brcmf_fweh_detach() is split, so fweh is freed only after the interfaces are gone and detach has cycled the wiphy lock. P2P creation also refuses to start once the bus is down, and looks up the primary interface only after that check.
  3. Close dynamic netdevs before detach unregisters them.
    • Race: detach reads iflist[i] and removes netdevs without RTNL or the wiphy lock, racing brcmf_p2p_del_vif(). Taking both locks would hang RTNL users: unregistering an UP netdev closes it, and cfg80211's NETDEV_GOING_DOWN handler takes the wiphy mutex again.
    • Fix: detach does what nl80211_del_interface() does: dev_close() under RTNL alone, then take the wiphy lock and unregister.
  4. pcie: dispatch RX only once the protocol is attached.
    • Race: setup sets the state UP before brcmf_attach(), and the IRQ thread re-arms whenever the state is UP. Two MSIs before preinit reach brcmf_proto_msgbuf_rx_trigger() with no protocol layer. A state left UP across a reset opens the same path during firmware download.
    • Fix: RX and re-arming are gated on a flag that preinit publishes after protocol initialization. The bus stop op clears it, then masks, synchronizes the IRQ, and masks again, before detach frees the protocol layer.

The crash

Without commits 1 and 4, a debugfs reset (echo 1 > /sys/kernel/debug/ieee80211/phyN/reset) on a BCM4387 with an AWDL interface up oopses in the IRQ thread:

Unable to handle kernel NULL pointer dereference at virtual address 0000000000000078
pc : brcmf_proto_msgbuf_rx_trigger+0x24/0x144 [brcmfmac]
lr : brcmf_pcie_isr_thread+0x108/0x164 [brcmfmac]
genirq: exiting task "irq/140-brcmf_p" (175906) is an active IRQ thread (irq 140)
ieee80211 phy2: brcmf_attach: dongle is not responding: err=-5

The faulting load is drvr->proto->pd with drvr->proto NULL. The machine later hung at shutdown. Which window the MSI arrived in, and what shutdown waited for, are unknown. This was reproduced once.

Testing

Setup: MacBook Pro 16-inch (M1 Pro, j316s), BCM4387/7, firmware 20.96.31.0.8.7.148, linux-aurora 7.2.11.aurora16 (bed544b). Modules built out of tree with clang 23.1.4, W=1 and CONFIG_BRCMDBG; the only warning is Kbuild's compiler-mismatch notice, which the base build also has. Every build includes the console-read bound from #453.

Each run performs one operation with an AWDL interface up and an AirDrop window open, then checks the loaded module's build ID, reattach (a fresh firmware banner), no Oops, WARNING, BUG, lockdep, hung-task, FW failed to initialize or Dongle setup failed message, Wi-Fi reassociation, and a new window with awdl0 UP.

Build debugfs reset unbind/bind modprobe -r
base Oops (above); attach fails not run not run
base + commit 1 pass not run not run
base + commits 1–3 pass pass pass
base + series pass pass pass

Each cell is a single run, so a pass shows only that the run recovered, not a failure rate.

What a pass doesn't show:

  • IOMMU faults: teardown logged new apple-dart 581008000.iommu: translation fault lines, which the run checks don't look for:

    Build reset unbind modprobe -r
    commits 1–3 2 1 5
    series 3 0 4
  • Other messages: teardown logged brcmf_msgbuf_delete_flowring: timed out waiting for txstatus, and attach logged add iface p2p-dev-wlan0 type 10 failed: err=-52. The latter also appears when the unmodified in-tree module loads.

  • AirDrop: reopening a window sometimes needed AWDL enable retries before frames were sent; after the full series' unbind it took three. Receive wasn't tested.

  • One unexplained failure: an earlier run of commits 1–3, without the wifi: brcmfmac: pcie: stop reading the firmware console at a bad index #453 bound and before the P2P lookup move, failed with FW failed to initialize. It wasn't reproduced.

Not tested: BCM4378, BCM4388, SDIO and USB buses, legacy (non-MSI) interrupts, suspend and resume, and an injected P2P_DEVICE create timeout racing detach.

Not fixed here

These are inherited from the base driver and each has its own issue:

brcmf_pcie_reset() masks the mailbox interrupt and calls brcmf_detach()
straight away. It leaves the PCIe state UP and does not wait for the
threaded IRQ handler. A handler that is already running sees UP, passes
RX to msgbuf, which can queue a firmware event while brcmf_detach() is
cancelling the event worker and freeing fweh, and then unmasks the
interrupt again.

Do what remove does before it detaches: set the state DOWN, mask, and
synchronize an allocated IRQ. A handler that read UP before the store can
still unmask after the first mask, so mask again once it has finished.
After that the handler neither dispatches RX nor unmasks.

The firmware has usually crashed by the time a reset runs, and remove
already detaches with the state DOWN, so ring writes failing with -EIO
during detach is the expected path.

Signed-off-by: Net Mojo <56850867+netmojo@users.noreply.github.com>
A P2P_DEVICE create holds the wiphy lock while it waits for the
firmware's IF_ADD event, and when the wait ends it calls
brcmf_fweh_p2pdev_setup(), which writes to drvr->fweh. A bus reset
enters brcmf_detach() without that lock. brcmf_fweh_detach() cancels the
event worker and frees fweh straight away, so a create that times out
after that writes to freed memory.

Split brcmf_fweh_detach(). brcmf_fweh_detach_events() cancels the event
worker where the old call was, so no IF event can reach an interface
that detach is removing. brcmf_fweh_detach() frees fweh after the
interfaces are gone.

Before it frees fweh, detach takes and releases the wiphy lock. P2P
creation now refuses to start unless the bus is up, and that check runs
under the wiphy lock, after detach has set BRCMF_BUS_DOWN. A create
already past the check holds the lock until it has finished with fweh,
so once detach has had the lock, nothing can still reach fweh.

Signed-off-by: Net Mojo <56850867+netmojo@users.noreply.github.com>
brcmf_detach() reads iflist[i] and ifp->ndev without RTNL or the wiphy
lock, then removes netdev interfaces unlocked. brcmf_p2p_del_vif(),
reached from NL80211_CMD_DEL_INTERFACE under both locks, can free a P2P
client or GO interface in that window. Only the P2P device interface,
which has no netdev, was removed under the locks.

Remove every interface except the primary one under RTNL and the wiphy
lock, and re-read the slot under them. Simply taking both locks around
brcmf_remove_interface(ifp, true) is not enough for a netdev that is up:
cfg80211_unregister_netdevice() closes it, and cfg80211's
NETDEV_GOING_DOWN and NETDEV_DOWN handlers take the wiphy mutex again.
The mutex is not recursive and RTNL is held, so every RTNL user on the
system hangs. awdl0 is a cfg80211 netdev and is up for the whole of an
AirDrop window, so a firmware crash during a window would reach this.

Do what nl80211_del_interface() does: dev_close() under RTNL alone,
then take the wiphy lock and unregister. brcmf_p2p_del_vif() needs RTNL
and the event worker has been cancelled, so the slot cannot change
between the two reads. The primary interface is still removed unlocked.
brcmf_del_if() takes RTNL itself to stop it.

Signed-off-by: Net Mojo <56850867+netmojo@users.noreply.github.com>
@netmojo
netmojo force-pushed the fix/brcmfmac-detach-races branch from 2bcffee to 2fd88bc Compare November 11, 2026 18:16
brcmf_pcie_setup() sets the PCIe state UP after the firmware download,
because ring setup needs it, and only then calls brcmf_attach(), which
creates the protocol layer. brcmf_pcie_preinit() enables the interrupt
once that is done. The IRQ thread does not wait for it: it re-arms the
interrupt whenever the state is UP, and dispatches RX whenever the state
is UP and irq_ready is set. One MSI between setup's UP and preinit makes
the thread re-arm and set irq_ready; the next calls
brcmf_proto_msgbuf_rx_trigger() while drvr->proto is NULL, or set but
without its msgbuf.

A state left UP across a bus reset opens the same path during the
firmware download. On a BCM4387 with a debugfs reset, running the
base tree plus only a separate console-read bound (without the three
preceding commits and without this one), the IRQ thread faulted
loading drvr->proto->pd:

  Unable to handle kernel NULL pointer dereference at virtual address 0000000000000078
  pc : brcmf_proto_msgbuf_rx_trigger+0x24/0x144 [brcmfmac]
  lr : brcmf_pcie_isr_thread+0x108/0x164 [brcmfmac]
  genirq: exiting task "irq/140-brcmf_p" (175906) is an active IRQ thread (irq 140)
  ieee80211 phy2: brcmf_attach: dongle is not responding: err=-5

Which of the two windows that MSI arrived in is not known.

Gate RX and re-arming on proto_ready. preinit publishes it after
brcmf_attach() has created the protocol layer, before it enables the
interrupt. The bus stop op clears it, masks, synchronizes the IRQ and
masks again, as reset now does. brcmf_detach() calls that op before it
frees the protocol layer, on reset, on remove, and when brcmf_attach()
fails. irq_ready alone cannot be the gate: the hard handler clears it
whenever it masks.

Signed-off-by: Net Mojo <56850867+netmojo@users.noreply.github.com>
@netmojo
netmojo force-pushed the fix/brcmfmac-detach-races branch from 2fd88bc to a9b802b Compare November 11, 2026 18:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant