前情回顾
在上篇说到,网卡在扣电池后终于能用了,但好景不长——系统开始不定期卡死,日志里一个 kernel NULL pointer dereference 告诉我,驱动还有问题
问题:系统到底怎么死的?
自从换了 hmtheboy154 的 mt7902e 驱动后,WiFi 确实能用了,但系统每三四天就会彻底锁死一次——鼠标能动但点不了任何东西,网断了,Ctrl+Alt+F3 切不了 TTY,只能长按电源键
查日志:
journalctl -b -1 -p 3结果:
kernel: BUG: kernel NULL pointer dereference, address: 0000000000000000kernel: 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 # 检查指针是否 NULLje 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,%rax25e53: 74 09 je 25e5e # 跳到 ret25e55: 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),%rax25e6d: eb e1 jmp 25e50 # 跳转至补丁区25e6f: 90 nop25e70: 90 nop25e71: 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; 就够了
补丁脚本和预编译模块已开源:
折腾永无止境,但至少这次我赢了 🤓
环境信息
系统: Arch Linux内核: 7.0.3-zen1-2-zen (SMP PREEMPT_DYNAMIC)主板: Gigabyte B850M FORCE WIFI6ECPU: 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提供技术支持
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时
