1117 字
3 分钟
折腾 MT7902 板载网卡 - 后续(修复内核空指针)
2026-05-09

前情回顾#

在上篇说到,网卡在扣电池后终于能用了,但好景不长——系统开始不定期卡死,日志里一个 kernel NULL pointer dereference 告诉我,驱动还有问题

问题:系统到底怎么死的?#

自从换了 hmtheboy154mt7902e 驱动后,WiFi 确实能用了,但系统每三四天就会彻底锁死一次——鼠标能动但点不了任何东西,网断了,Ctrl+Alt+F3 切不了 TTY,只能长按电源键

查日志:

journalctl -b -1 -p 3

结果:

kernel: BUG: kernel NULL pointer dereference, address: 0000000000000000
kernel: RIP: mt7921_channel_switch_rx_beacon+0x9/0x90 [mt7902e]

内核空指针,每次都在同一个函数:mt7921_channel_switch_rx_beacon

这函数干啥的?当 AP 发信道切换信标(Channel Switch Announcement)时,驱动要处理这个信标,但代码里少写了一行判空,取指针时拿到 NULL,直接解引用,炸了

先确认一下:真的是同个 bug 吗?#

把驱动模块反汇编出来看看:

objdump -d mt7902e.ko | grep -A 20 'channel_switch_rx_beacon'
25e69: mov 0x58(%rdi),%rax # phy → [0x58]
25e6d: mov 0x8(%rax),%rax # [0x08] → 这里如果上面是 NULL,就崩了
25e71: mov 0x92c8(%rax),%rax # [0x92c8]
25e78: mov (%rax),%rcx # 解引用指针

三层指针链,中间任何一层没判空就往下走,撞上 NULL 直接 Oops。

一个月里崩了 8 次,基本稳定复现。最后那个偏移从旧内核的 0x9058 变到了 0x92c8,但逻辑纹丝不动——这驱动是 2025 年 12 月编译的,那时候主线还没修这个 bug

试过的方案#

1. 换主线内核的驱动#

升级内核到 7.0.3-zen,指望主线自带的 mt7921e 能认这张卡——结果 /lib/modules/.../mt76/mt7921/ 下只有 mt7921s.ko(SDIO)和 mt7921u.ko(USB),PCIe 版本的 mt7921e.ko 压根没编进去。stock kernel 把 MT7902 的 PCI ID 漏了

2. 小米 BSP 驱动(FullMAC)#

从开发者仓库拉了一个 mt7902 gen4 BSP 驱动,编是能编过:

make # 200+ 个 .c 文件,零错误通过

结果 modprobe mt7902 之后系统直接死了,TTY 都切不了,比我之前的 NULL 指针还狠。查了下这驱动是 FullMAC 架构(固件处理大部分工作),和当前内核的无线栈有符号冲突,加载时就报了 duplicate symbol 警告,强行跑直接崩系统

这条路线放弃

最终方案:二进制打补丁#

既然源码不想动(那是别人的驱动树,fork 了还得维护),而且 bug 已经定位到了——就一行 if(!ptr) return; 的事,那直接在编译好的 .ko 里改两字节就行

思路#

函数开头取完第一级指针后跳转到补丁代码区,检查是否 NULL:

正常路径:取指针 → test → 非空 → 继续原逻辑
安全路径:取指针 → test → 是 NULL → ret(返回)

在 ELF 文件里找到可以放补丁代码的空地——每个函数前有一个 __pfx 前缀区域,供内核 CFI 用的,里面全是 16 个 0x90(NOP)。这个模块没启用 CFI,所以这 16 个字节永远不会被执行,正好拿来放代码

改了什么#

补丁区(__pfx 区域,原 16 个 NOP):

test %rax,%rax # 检查指针是否 NULL
je ret_label # 是 NULL → 返回
mov 0x8(%rax),%rax # 原指令(现在安全了)
jmp 原函数继续点 # 跳回去
ret_label:
ret # 安全返回

函数体里:

# 把原来直接解引用的 `mov 0x8(%rax),%rax` 替换成
jmp 补丁区

两处改动,总共改了几个字节。用 Python 脚本直接写进 .ko.zst

# 伪代码示意,实际改了 16+4=20 字节
patch_pfx = bytes([
0x48, 0x85, 0xc0, # test %rax,%rax
0x74, 0x09, # je +9 (跳过4条指令到ret)
0x48, 0x8b, 0x40, 0x08, # mov 0x8(%rax),%rax
0xe9, 0x13, 0x00, 0x00, 0x00, # jmp 回原函数
0xc3, # ret
0x90 # nop 填充
])
patch_func = bytes([0xeb, 0xe1, 0x90, 0x90]) # jmp 到补丁区

验证#

打补丁后用 objdump 确认逻辑正确:

25e50: 48 85 c0 test %rax,%rax
25e53: 74 09 je 25e5e # 跳到 ret
25e55: 48 8b 40 08 mov 0x8(%rax),%rax # 原有指令
25e59: e9 13 00 00 00 jmp 25e71 # 跳回原函数
25e5e: c3 ret # 安全出口
25e5f: 90 nop
25e69: 48 8b 47 58 mov 0x58(%rdi),%rax
25e6d: eb e1 jmp 25e50 # 跳转至补丁区
25e6f: 90 nop
25e70: 90 nop
25e71: 48 8b 80 c8 92 00 00 mov 0x92c8(%rax),%rax # 继续正常流程

结果#

重启后加载的已经是被打补丁的驱动,运行正常

不会更糟糕的理由:

  • 指针正常时,行为零变化——指令改成了跳转到补丁区,补丁区原封不动执行了原指令,再跳回来,和没改一样
  • 指针 NULL 时,直接 ret 返回,跳过整个信标处理。最坏情况就是信道切换时 WiFi 断一下重连,但系统不会再锁死了
  • 补丁写在 __pfx 区域,那 16 个字节本来就永远不会被执行,改了就改了
  • .ko.zst 文件随时可以删掉重装 linux-zen 包恢复原状

唯一要注意的#

以后 pacman -Syu 更新内核后(比如升级到 7.0.4-zen),新内核有自己的 /lib/modules/ 目录,补丁就丢了。需要对新内核的 .ko 重新打一次。不过也就跑一遍脚本的事

复盘#

方案结果评价
主线 mt7921e内核没编这个模块等上游修
gen4 BSP 驱动系统直接炸架构不兼容
二进制打补丁零副作用修复简单粗暴有效

20 个字节修了一个月崩了 8 次的 bug。有时候解决问题不一定需要重写整个驱动,一行 if (!ptr) return; 就够了

补丁脚本和预编译模块已开源:

GT001well
/
mt7902e-null-fix
Waiting for api.github.com...
00K
0K
0K
Waiting...

折腾永无止境,但至少这次我赢了 🤓

环境信息#

系统: Arch Linux
内核: 7.0.3-zen1-2-zen (SMP PREEMPT_DYNAMIC)
主板: Gigabyte B850M FORCE WIFI6E
CPU: AMD Ryzen 7 9700X 8-Core
内存: 30GB
网卡: MediaTek MT7902 (Filogic 310) PCIe [14c3:7902]
驱动: mt7902e.ko (编译于 2025-12-12)
固件: mediatek/WIFI_MT7902_patch_mcu_1_1_hdr.bin.zst
mediatek/WIFI_RAM_CODE_MT7902_1.bin(系统缺失,但驱动仍可用)
补丁仓库:
::github{repo="GT001well/mt7902e-null-fix"}

Saya提供技术支持

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

折腾 MT7902 板载网卡 - 后续(修复内核空指针)
https://text.lilystar.cn/posts/mt7902-后续/
作者
Lily
发布于
2026-05-09
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录