路由器开机启动过程中,概率性出现panic,日志如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | CPU 2 Unable to handle kernel paging request at virtual address d0c17558, epc == 8ce01110, ra == 8ce010d4 Oops[#1]: CPU: 2 PID: 6392 Comm: trafficd Not tainted 4.4.198 #0 task: 8fedc860 task.stack: 8b0ac000 $ 0 : 00000000 7705f098 00000001 d0c17548 $ 4 : 0a1f0337 8ce09c40 00000000 00000000 $ 8 : 00000000 ffffffff 00000000 0000003c $12 : 00000001 00000000 00000000 00000000 $16 : 8ce10e68 8ce00000 8ce00000 8ce10000 $20 : 8ce10000 8ce00000 00000000 8ce01c20 $24 : 00000000 8105596c $28 : 8b0ac000 8fc0fe88 8184a884 8ce010d4 Hi : 00000000 Lo : 0000bc00 epc : 8ce01110 0x8ce01110 ra : 8ce010d4 0x8ce010d4 Status: 11007c03 KERNEL EXL IE Cause : 40800008 (ExcCode 02) BadVA : d0c17558 PrId : 0001992f (MIPS 1004Kc) Modules linked in: Process trafficd (pid: 6392, threadinfo=8b0ac000, task=8fedc860, tls=7707adc0) Stack : 00000002 8184a884 00000005 8fc4a804 81850000 8fc4a430 81740000 8104d6c0 304d9b80 81ec2120 30e63200 0000000c 00000001 81c90000 00000100 81850000 8ce009ec 81ec1940 81ec1a40 81ec1340 00200000 00000200 00100000 8107a9a4 818591fc 81c22ec0 00000008 00000001 81853c20 8184a884 81ec1320 81850000 81ec1840 8107ac4c 0000003a 8106c000 00000001 00000001 00000001 81ec1740 ... Call Trace: [<8ce01110>] 0x8ce01110 Code: 00041940 02031821 8cab0008 <8c660010> 8c6c0014 8cad000c 00cb5821 018d6021 0166302b ---[ end trace eb3bf2a8ccaf6627 ]--- |
Call trace没有打印出符号,上一篇文章[1],已经根据panic信息中的Code指令定位到了出现问题的内核模块。对其进行反汇编如下:
$ mipsel-openwrt-linux-objdump -S xxx.ko
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | for (i = 0; i < n; i++) { if ((table->netmask & node[i].ip) != table->net) 10d4: 8e070008 lw a3,8(s0) 10d8: 8e080004 lw t0,4(s0) continue; slot = node[i].ip & ~ table->netmask; 10dc: 00074827 nor t1,zero,a3 10e0: 26858020 addiu a1,s4,-32736 for (i = 0; i < n; i++) { 10e4: 00005025 move t2,zero 10e8: 0142182a slt v1,t2,v0 10ec: 1060fe7f beqz v1,aec <stat_timer_cb+0x100> 10f0: 00000000 nop if ((table->netmask & node[i].ip) != table->net) 10f4: 8ca40000 lw a0,0(a1) 10f8: 00e41824 and v1,a3,a0 10fc: 1468002e bne v1,t0,11b8 <stat_timer_cb+0x7cc> 1100: 00892024 and a0,a0,t1 table->hnat_stats[slot].src_bytes += node[i].src_bytes; 1104: 00041940 sll v1,a0,0x5 1108: 02031821 addu v1,s0,v1 110c: 8cab0008 lw t3,8(a1) 1110: 8c660010 lw a2,16(v1) 1114: 8c6c0014 lw t4,20(v1) 1118: 8cad000c lw t5,12(a1) 111c: 00cb5821 addu t3,a2,t3 1120: 018d6021 addu t4,t4,t5 1124: 0166302b sltu a2,t3,a2 1128: 00cc3021 addu a2,a2,t4 112c: ac6b0010 sw t3,16(v1) 1130: ac660014 sw a2,20(v1) |
代码及数据结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | struct ip_acc_table { uint32_t ip, net, netmask; /* increase by 1 when timer run userspace tool can check this field to know that ip account update a rate */ unsigned cursor; struct ip_acc_stats hnat_stats[MAX_IP_NUM]; struct ip_acc ip_acc[MAX_IP_NUM]; struct ip_acc_mask_24 __percpu *stats; }; struct hnat_speed_node { uint32_t ip; u64 src_bytes, dst_bytes, src_pkts, dst_pkts; }; struct ip_acc_stats { uint64_t src_bytes; uint64_t dst_bytes; uint64_t src_pkts; uint64_t dst_pkts; }; static struct hnat_speed_node node[N]; static void mtk_hnat_v1_stat( struct ip_acc_stats *stats) { int i, n; unsigned slot; struct ip_acc_table *table = &ip_acc_table_default; n = hnat_sync_speed_table_hook(node); for (i = 0; i < n; i++) { if ((table->netmask & node[i].ip) != table->net) continue ; slot = node[i].ip & ~ table->netmask; table->hnat_stats[slot].src_bytes += node[i].src_bytes; } } |
node是一个全局数组,table是一个全局变量。寄存器和变量的对应关系得多分析几遍才能慢慢确定。确定好后,就可以顺腾摸瓜,计算出各个变量的值了。非法内存访问,基本上就是内存越界了。
10d4/10d8处的两条指令:
lw a3, 8(s0)
lw t0, 4(s0)
lw是load word指令,读取s0寄存器表示的内存的8和4偏移的内存到寄存器a3和t0。
像这种带偏移的指令,通常是读取结构体的成员。观察上述结构体,table的net和netmask偏移刚好是4和8。和反汇编的源码也对应的上。
因此可以推断:s0是table的地址,a3是读取netmask,t0是读取net。
根据参考[2],s0是16号寄存器,call trace中,其值为:0x8ce10e68。
a3是7号寄存器,其值为0,t0是8号寄存器,其值也为0。根据后续指令,a3和t0在panic之前,没有被覆盖,因此可以推断:
table->net值为0,table->netmask值为0。
10dc处的指令:
nor t1,zero,a3
nor指令是,两个寄存器先或,再取反。这条指令等价于 t1 = ~(0 | a3)
a3是table->netmask。因此t1的值是 ~tables->netamsk,为0xffffffff。
t1是9号寄存器,call trace中的值也是0xffffffff,逻辑关系对应得上。
10e0处的指令:
addiu a1,s4,-32736
搞不太懂,先跳过
10e4处的指令:
move t2,zero
将t2指令设置为0,根据后续指令,可以推断,这条指令对应:i = 0,
t2寄存器存储变量i的值。t2是10号寄存器,call trace中10号寄存器的值是0,也就是说i等于0的时候,就崩溃了。
10e8处的指令:
slt v1,t2,v0
slt指令,是如果第一个源寄存器的值小于第二个源寄存器的值,则将目标寄存器设置为1。
这显然对应for 语句中的条件判断:i < n。
t2是i,n是v0,v0是2号寄存器,call trace中2号寄存器的值是1,也就是说n的值是1。
结果v1的值为1,v1是3号寄存器,call trace中3号寄存器的值不是1,因为后续指令把v1的值覆盖了。
10ec处的指令:
beqz v1,aec <stat_timer_cb+0x100>
b是跳转指令,这个显然是 i < n 失败时,跳转结束循环。
10f4、10f8、10fc处的指令:
lw a0,0(a1)
and v1,a3,a0
bne v1,t0,11b8
a1是在10e0处设置的,猜不到是什么值。但是可以根据下2条指令
v1 = a3 & a0
if (v1 != t0)
goto 11b8
11b8处指令:
addiu t2,t2,1
t2是i,也就是i = i + 1,因此可以推断上述这3条指令,对应:
if ((table->netmask & node[i].ip) != table->net)
continue;
前面已经得到a3是table->netmask,t0是table->net,那a0就是node[i].ip,a1就是&node[i]。
a0是4号寄存器,值是0x0a1f0337,这是一个主机字节序的ip地址值,也就是对应ip:10.31.3.55
这里因为netmask和net都是0,因此条件不成立,不会走continue
1100指令:
and a0,a0,t1
等价于:a0 = a0 & t1,
a0是node[i].ip,t1是~table->netmask。这条指令也就是:
slot = node[i].ip & ~ table->netmask;
因此a0的值,0x0a1f0337,因为t1是全1,所以a0的值不变。到这里其实问题已经明朗了,slot的合法范围是0到255.
这里存在严重的越界。
1104-1110 4条指令:
sll v1,a0,0x5
addu v1,s0,v1
lw t3,8(a1)
lw a2,16(v1)
这4条指令是读取table->hnat_stats[slot].src_bytes和node[i].src_bytes的值
hnat_stats结构体大小是32字节,所以hnat_stats[slot]的偏移是32 * slot也就是 slot << 5
hnat_stats在table中的偏移是16. 所以内存地址是 table + 16 + (slot << 5)
table是s0,slot是a0。
panic发生在1110指令处,读取16(v1)内存的时候,访问非法内存指令:
s0 = 0x8ce10e68
a0 = 0x0a1f0337
计算出非法内存地址:0x8ce10e68 + 16 + (0x0a1f0337 << 5) = d0c17558
和panic报告的内存地址完全对应上了。
结论:
panic是由于开机过程中,table->net和table->netmask还未初始化,值为0,导致统计ip的时候,计算出的slot越界。
参考:
[1] no stack trace kernel panic analysis. https://insidelinuxdev.net/article/a0fbgf.html
[2] mips架构. https://insidelinuxdev.net/article/a06pxn.html
[3] nor instruction. https://www.eecis.udel.edu/~davis/cpeg222/AssemblyTutorial/Chapter-12/ass12_10.html
[4] slt instruction. https://www3.ntu.edu.sg/home/smitha/fyp_gerald/sltInstruction.html
[5] sll instruction. https://people.cs.pitt.edu/~childers/CS0447/lectures/shift-operations.pdf
[6] lui instruction. https://stackoverflow.com/questions/8380135/mips-assembly-lui-t0-4097