mt7996: NULL deref in mac80211 restart work on failed MCU init (WED disabled)

Open
#1,148 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
48/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Active
Tech stack
c, linux
Domain
backend

Research direction

Read mt7996_mac_full_reset(), mt7996_mac_reset_work(), and the hw_init_done guard in mt7996_reset(); the issue identifies these entry points but no test file. Check whether the failing-probe path can call ieee80211_restart_hw() before ieee80211_register_hw(), and compare the lifecycle behavior with commit 3def033. Done means the reset path cannot queue restart work against an unregistered device; validate with a failed MCU-init/probe scenario.

Written by the indexing model from the issue text.

Description

Sibling of #1147 — same board and trigger, but a different crash, and it reproduces with the #1147 workaround (WED disabled) already applied.

Summary

On a Banana Pi BPI-R4 (MT7988A + BE14 / MT7996), with WED disabled, a failed
MCU firmware-patch download on a warm reboot ends in a kernel NULL-pointer Oops
in mac80211, not in the mt76 WED path. The driver's full-reset handler calls
ieee80211_restart_hw() even though the device never finished probing (probe
later returns -EAGAIN), so ieee80211_restart_work runs against a hw that was
never registered and dereferences a NULL local->workqueue
(flush_workqueue(NULL) -> fault at 0x20 = offsetof(workqueue_struct, mutex)).

This is a sibling of the WED crash reported in #1147 (same trigger, same
board, same firmware timeout) but a different crash signature: #1147 faults
at 0x58 in mt76_dma_queue_reset() on a WED queue with q->wed->ops == NULL;
this one faults at 0x20 in mac80211's restart work with WED off.

Environment

  • Board: Banana Pi BPI-R4 (2x SFP+), MT7988A, BE14 card (MT7996), hif2 on PCI
    domain 1
  • OpenWrt 25.12.5, kernel 6.12.94 (backports from v6.18.26)
  • out-of-tree openwrt/mt76 snapshot 39c960c3 (2026-03-19); MT7996 firmware
    build 20251226
  • mt7996e wed_enable=N — WED disabled. /sys/module/mt7996e/parameters/wed_enable
    = N; /etc/modules.d/mt7996e is a plain mt7996e with no options. No
    attaching wed device ... lines appear at boot (contrast with #1147, where
    they did).
  • Happened after several warm reboots in a short window. A cold boot (power
    off) recovered. Same as the #1147 trigger.

What changed since #1147

In #1147 the fix we applied on our side was to disable WED. That removed the
0x58 WED-path crash — but a different crash now appears on the exact same
MCU-timeout trigger. So disabling WED is not a complete workaround for this
class of failure; it just moves the crash from the mt76 WED path into mac80211's
hardware-restart path.

dmesg (relevant excerpt, from pstore)

[   17.221620] mt7996e 0000:01:00.0: HW/SW Version: 0x8a108a10, Build Time: 20251226095555a
[   17.342597] sfp sfp1: module FS               XGS-SFP-ONT-MACI rev 0001 sn <redacted>      dc 260901
[   17.371460] sfp sfp2: module OEM              SFP-2.5G-T-R-RM  rev 1.0  sn <redacted>       dc 250408
[   22.311306] mt7996e 0000:01:00.0: Message 00000007 (seq 13) timeout
[   22.317584] mt7996e 0000:01:00.0: Failed to start patch
[   22.322814] mt7996e 0000:01:00.0: Failed to release patch semaphore
[   27.431300] mt7996e 0000:01:00.0: Message 00000010 (seq 15) timeout
[   27.437577] mt7996e 0000:01:00.0: Failed to get patch semaphore
[   32.551298] mt7996e 0000:01:00.0: Message 00000010 (seq 1) timeout
[   32.557483] mt7996e 0000:01:00.0: Failed to get patch semaphore
       ... (8 more identical "Failed to get patch semaphore", ~5.12 s apart) ...
[   73.511308] mt7996e 0000:01:00.0: Message 00000010 (seq 9) timeout
[   73.517498] mt7996e 0000:01:00.0: Failed to get patch semaphore
[   73.523449] mt7996e 0000:01:00.0: chip full reset failed
[   73.528759] ieee80211 phy0: Hardware restart was requested
[   73.534265] Unable to handle kernel read from unreadable memory at virtual address 0000000000000020
[   73.543377] mt7996e 0000:01:00.0: probe with driver mt7996e failed with error -11
[   73.546084]   ESR = 0x0000000096000005
[   73.562619]   EC = 0x25: DABT (current EL), IL = 32 bits
[   73.568798]   FSC = 0x05: level 1 translation fault
[   73.609431] Internal error: Oops: 0000000096000005 [#1] SMP
[   73.667324] CPU: 2 UID: 0 PID: 242 Comm: kworker/2:7 Tainted: G           O       6.12.94 #0
[   73.684702] Workqueue: events_freezable 0xffffffc078efe180 [mac80211@...+0x7c000]
[   73.700260] pc : 0xffffffc08094e28c
[   73.703738] lr : 0xffffffc08005850c
[   73.724771] x26: ffffff80c114a280 x25: ffffff80c114a2c0 x24: 0000000000000020
[   73.781774] x2 : ffffff80c18b0000 x1 : 0000000000000000 x0 : 0000000000000020
[   73.781774] Call trace:
[   73.784210]  0xffffffc08094e28c
[   73.787341]  0xffffffc078efe1b4 [mac80211@...+0x7c000]
[   73.793597]  0xffffffc080059ef4          (process_one_work)
[   73.796727]  0xffffffc08005ab8c          (worker_thread)
[   73.799857]  0xffffffc080061b1c          (kthread)
[   73.802988]  0xffffffc0800145a0          (ret_from_fork)
[   73.806120] Code: d503201f d2800001 d5384102 f9800011 (c85ffc03)
[   73.829678] Kernel panic - not syncing: Oops: Fatal exception

(Kernel has no kallsyms; module-relative addresses only. The bare 0xffffffc080...
addresses are vmlin.)

Root-cause hypothesis

  1. The MCU never answers the firmware patch download (Message 7 ... Failed to start patch). The driver enters a full reset and retries the whole firmware
    load — the ten Failed to get patch semaphore lines, ~5.12 s apart, are the
    for (i = 0; i < 10; i++) mt7996_mac_restart() loop in
    mt7996_mac_full_reset(). All ten time out -> chip full reset failed.
  2. mt7996_mac_full_reset() then calls ieee80211_restart_hw()
    unconditionally (printing "Hardware restart was requested"), which queues
    ieee80211_restart_work on system_freezable_wq.
  3. But the probe had failed at the MCU stage and is about to return -EAGAIN
    (probe ... failed with error -11) — ieee80211_register_hw() was never
    called
    , so local->workqueue is still NULL.
  4. ieee80211_restart_work's first statement is
    flush_workqueue(local->workqueue) -> mutex_lock(&NULL->mutex). The fault
    VA 0x20 is exactly offsetof(struct workqueue_struct, mutex)
    (pwqs 16B + list 16B), and the Code: bytes
    ... f9800011 (c85ffc03) are the arm64 mutex_lock() fast-path
    prfm/ldaxr with x0 = 0x20.

So: a failed/aborted MCU init requests a mac80211 hardware restart against a
device that was never registered, and the restart work dereferences a NULL
local->workqueue.

Confidence: trigger = proven from the log; fault mechanism = strongly supported
from the register/offset/code-byte match against mainline 6.12 source. I could
not disassemble the exact line — I don't have the matching mac80211.ko for
this build (only the mt76 binaries), so this is source-pinned, not
binary-pinned.

Possible fix direction (hypothesis only, not a patch)

A natural guard would be to not request the hardware restart when the device
never finished init — e.g. gate the ieee80211_restart_hw() calls in
mt7996_mac_full_reset() (and/or bail out of mt7996_mac_reset_work()) on
dev->recovery.hw_init_done, mirroring the guard that already exists at the top
of mt7996_reset(). Equivalently, avoid scheduling/entering the full-reset path
at all while a probe is still in flight. I haven't written or tested a diff, and
I'm not sure where the 39c960c3 snapshot actually lets reset_work start
during a failed probe (in mainline mt7996_reset() returns early when
hw_init_done is false) — so I'd rather ask than guess.

Question for the maintainer

With WED disabled, a failed MCU patch download during probe ends in
mt7996_mac_full_reset() calling ieee80211_restart_hw() on a device that
never reached ieee80211_register_hw(), and ieee80211_restart_work then
NULL-derefs local->workqueue. Is the full-reset / ieee80211_restart_hw()
path expected to run while probe is still failing (before registration)? Should
mt7996_mac_full_reset() be guarded on recovery.hw_init_done the way
mt7996_reset() already is, or is the intended fix elsewhere (e.g. not starting
reset_work at all during a failed init)? Does 3def033 ("cancel reset and rc
work on device unregister") cover the never-registered case, or only
unregister?

I'm happy to apply a patch and test it, or share the full pstore records and the
module binaries.

Dominant language
C
Stars
895
Forks
461
PR merge metrics
No merged PRs in 30d

Getting set up

This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from openwrt/mt76

All issues in openwrt/mt76

Similar issues

More C issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.