<1>.现场分析 ,log信息如下:
3 [ 60.623647@0] Unable to handle kernel paging request at virtual address ef800004
4 [ 60.625413@0] pgd = e01a0000
5 [ 60.628250@0] [ef800004] *pgd=00000000
6 [ 60.631962@0] Internal error: Oops: 5 [#1] PREEMPT SMP ARM
7 [ 60.637394@0] Modules linked in: mali ufsd(PO) jnl(O) aml_nftl_dev(P)
8 [ 60.643779@0] CPU: 0 PID: 120 Comm: kworker/0:2 Tainted: P O 3.10.33 #24
9 [ 60.651290@0] Workqueue: events di_timer_handle
10 [ 60.655765@0] task: e12a4780 ti: e0c18000 task.ti: e0c18000
11 [ 60.661289@0] PC is at strcmp+0x0/0x3c
12 [ 60.664996@0] LR is at vf_get_provider_name+0x64/0xc8
13 [ 60.669997@0] pc : [] lr : [] psr: 800e0193
14 [ 60.669997@0] sp : e0c19dd0 ip : 00000016 fp : 34353530
15 [ 60.681726@0] r10: ef800004 r9 : 73202c30 r8 : c11c3488
16 [ 60.687073@0] r7 : 00000000 r6 : e1a12800 r5 : c0d197b4 r4 : 00238228
17 [ 60.693714@0] r3 : 00000000 r2 : 00000064 r1 : c0d197b4 r0 : ef800004
18 [ 60.700356@0] Flags: Nzcv IRQs off FIQs on Mode SVC_32 ISA ARM Segment kernel
19 [ 60.707863@0] Control: 10c5387d Table: 203a004a DAC: 00000015
”Unable to handle kernel paging request at virtual address ef800004“,一般发生在对应的kernel 地址没有对应的page或者有对应的page但没有相应的权限(如,写只读权限的映射)。
查看对应发生异常的对应的代码位置:
200 char* vf_get_provider_name(const char* receiver_name)
201 {
202 int i,j;
203 char* provider_name = NULL;
204 for (i = 0; i < vfm_map_num; i++) {
205 if (vfm_map[i] && vfm_map[i]->active) {
206 for (j = 0; j < vfm_map[i]->vfm_map_size; j++) {
207 if (!strcmp(vfm_map[i]->name[j], receiver_name)) {
208 if ((j > 0)&&((vfm_map[i]->active>>(j-1))&0x1)) {
209 provider_name = vfm_map[i]->name[j - 1];
210 }
211 break;
212 }
213 }
214 }
215 if (provider_name)
216 break;
217 }
218 return provider_name;
219 }
通过arm-linux-gnueabihf-gdb vmlinux查看对应的代码207行的strcmp函数的第一行。初步判定是是第一个参数指针非法导致的且指针的值为crash log里面的0xef800004。
根据代码vfm_map_t* vfm_map[VFM_MAP_COUNT],可知vfm_map是vfm_map_t类型的数组,数组大小为VFM_MAP_COUNT = 20。vfm_map_t结构体如下:
40 typedef struct{
41 char id[VFM_NAME_LEN];
42 char name[VFM_MAP_SIZE][VFM_NAME_LEN];
43 int vfm_map_size;
44 int valid;
45 int active;
46 }vfm_map_t;
看strcmp函数的第一个参数,vfm_map[i]->name[j],该指针的值为,vfm_map[i] +j*VFM_NAME_LEN这个值等于0xef800004,也就是非法的。vfm_map[i]等于vfm_map + i *sizeof(struct vfm_map_t),最终第一个参数的指针值为
vfm_map + i *sizeof(struct vfm_map_t) + j *VFM_NAME_LEN。首先是vfm_map的值没有被改变,他是static分配的,i是数组里vfm_map_t元素的个数,经打印一般是4,也没有改变。现在是整个地址非法,那么j的异常的可能性最大,经打印j可能会达到400左右,远远大于合法值(VFM_MAP_SIZE = 10)。所以,初步结论是vfm_map[i]->vfm_map_size的值被非法篡改了,导致j 比较大,最后导致vfm_map[i]->name[j]值指向vfm_map合法内存后面很远的地方(非法地址0xef800004)。
修改内核程序,将vfm_map_t结构体加上maigic验证,如下:
40 typedef struct{
41 char id[VFM_NAME_LEN];
42 char name[VFM_MAP_SIZE][VFM_NAME_LEN];
43 int magic0;
44 int vfm_map_size;
45 magic1;
46 int valid;
46 int active;
48 }vfm_map_t;
也就是在vfm_map_size前后加入分别加入magic0和magic1 ,其中#define magic0 0x12345678 #define magic1 0x87654321。
重新运行程序,等待crash复现,最终发现crash发生的时候,magic0和magic1全部被篡改,据此推断是连续的内存被覆写,很像memcpy这类拷贝函数的越界内存连续写造成的结果。
<2>.jprobe动态注入memcpy函数hook代码
由于怀疑是memcpy函数的写越界导致的内存踩踏,那么可以在memcpy函数里面加入自己的hook代码,只要该函数写的范围包含magic0或者magic1所在的地址,就dump_stack打印出当前调用栈,基本就可以锁定是谁破坏的。
内核为了效率,memcpy完全是有汇编实现,加入c代码很困难。可以采用jprobe技术,动态注册jprobe,将memcpy函数劫持,在jprobe的注册回调里面执行hook代码。源码如下:
1 #include
2 #include
3 #include
4 #include
5
6 #define VFM_NAME_LEN 100
7 #define VFM_MAP_SIZE 10
8 #define VFM_MAP_COUNT 20
9
10 typedef struct{
11 char id[VFM_NAME_LEN];
12 char name[VFM_MAP_SIZE][VFM_NAME_LEN];
13 int magic0;
14 int vfm_map_size;
15 int magic1;
16 int valid;
17 int active;
18 }vfm_map_t;
19
20 vfm_map_t **vfm_map;
21 unsigned int probed = 0;
22 static unsigned long count0 = 0;
23 static unsigned long watch_addr = 0;
24 static long my_memcpy(void *dst, const void *src, __kernel_size_t count)
25 {
26
27 if(((unsigned long)dst <= watch_addr) && (((unsigned long)dst +count)>= watch_addr)){
28 count0++;
29 printk("bug +++++++++ dst %lx src %lx count %ld!\n",(unsigned long)dst,(unsigned long)src,(unsigned long)count);
30 dump_stack();
31 }
32 jprobe_return();
33
34 return 0;
35
36 }
37 static struct jprobe my_jprobe = {
38 .entry = my_memcpy,
39 .kp = {
40 .symbol_name = "memcpy",
41 },
42 };
43
44 static int __init jprobe_init(void)
45 {
46 int ret;
47 if(!(vfm_map = (vfm_map_t*)kallsyms_lookup_name("vfm_map"))){
48 printk("vfm_map can not found!\n");
49 return 0;
50 }
51 printk("vfm_map %lx!\n",(unsigned long)(vfm_map));
52 probed = 1;
53 watch_addr = (unsigned long)(&(vfm_map)[1]->magic0);
54 printk("watch_addr %lx--------------------------------------------------------------------------!\n",watch_addr);
55
56 ret = register_jprobe(&my_jprobe);
57 if (ret < 0) {
58 printk(KERN_INFO "register_jprobe failed, returned %d\n", ret);
59 return -1;
60 }
61 printk(KERN_INFO "Planted jprobe at %p, handler addr %p\n",
62 my_jprobe.kp.addr, my_jprobe.entry);
63 return 0;
64 }
65
66 static void __exit jprobe_exit(void)
67 {
68 if(!probed)
69 return;
70 unregister_jprobe(&my_jprobe);
71 printk(KERN_INFO "jprobe at %p unregistered\n", my_jprobe.kp.addr);
72 }
73
74 module_init(jprobe_init)
75 module_exit(jprobe_exit)
76 MODULE_LICENSE("GPL");
76,5 底端
<3>.jprobe爆出的调用栈分析
在加入jprobe注册之后。等待crash复现,最终爆出的调用栈如下:
[ 28.503307@1] eth0: device MAC address b0:d5:9d:b4:f6:d2
[ 28.516553@1] stmmac_open: failed PTP initialisation
[ 30.592329@0] bug +++++++++ dst e1a11000 src f000833c count 15556!
[ 30.592883@0] CPU: 0 PID: 751 Comm: mount Tainted: P O 3.10.33 #63
[ 30.599887@0] [] (unwind_backtrace+0x0/0xec) from [] (sh)
[ 30.608502@0] [] (show_stack+0x10/0x14) from [] (my_memc)
[ 30.617298@0] [] (my_memcpy+0x54/0x64 [probe]) from [] ()
[ 30.627384@0] [] (persistent_ram_save_old+0x124/0x148) from [] (ramoops_pstore_read+0x90/0x180) from [)
[ 30.647564@0] [] (pstore_get_records+0xf0/0x148) from [])
[ 30.657143@0] [] (pstore_fill_super+0xa8/0xbc) from [] ()
[ 30.666112@0] [] (mount_single+0x58/0xd0) from [] (mount)
[ 30.674392@0] [] (mount_fs+0x70/0x170) from [] (vfs_kern)
[ 30.682844@0] [] (vfs_kern_mount+0x4c/0xcc) from [] (do_)
[ 30.691379@0] [] (do_mount+0x784/0x8a8) from [] (SyS_mou)
[ 30.699488@0] [] (SyS_mount+0x84/0xb8) from [] (ret_fast)
[ 30.708068@0] ------------[ cut here ]------------
my_memcpy是我在memcpy函数的里的jprobe回调。这个栈能爆出来说明magic0被改变了,在爆出这个栈之后,随后立马复现了以前的内存crash,说明这是破坏第一现场。
根据栈分析,这是在挂载pstore 文件系统到/sys/fs/pstore目录的时候在mount系统调用里面调用memcpy导致内存越界写到vfm模块的vfm_map数组内存,导致内存crash。
pstore文件系统是内存文件系统。ramoops是框架是基于pstore文件系统来存储上次的crash的dmesg信息。ramoops通过在cmdline里面为最近预留1m内存,在系统oops或者panic的时候将crash的日志同步写到pstore文件系统里面,也就是写到pstore文件系统挂在目录下的文件里面dmesg_ramoops-*文件下。另外还有console_ramoops_*文件用来实施收集dmesg的信息,无论是否发生panic都会保存。
问题就出在pstore_ramoops的persistent_ram_save_old函数里面。函数代码如下:
305 void persistent_ram_save_old(struct persistent_ram_zone *prz)
306 {
307 struct persistent_ram_buffer *buffer = prz->buffer;
308 size_t size = buffer_size(prz);
309 size_t start = buffer_start(prz);
310
311 if(atomic_read(&prz->buffer->full_flag))
312 {
313 size = prz->buffer_size;
314 // printk("^^^^^^^^^^^^^^size=0x%x",size);
315 }
316
317 if (!size)
318 return;
319
320 if (!prz->old_log) {
321 persistent_ram_ecc_old(prz);
322 prz->old_log = kmalloc(size, GFP_KERNEL);
323 }
324 if (!prz->old_log) {
325 pr_err("persistent_ram: failed to allocate buffer\n");
326 return;
327 }
328
329 prz->old_log_size = size;
330 memcpy(prz->old_log, &buffer->data[start], size - start);
331 memcpy(prz->old_log + size - start, &buffer->data[0], start);
332 }
该函数主要是将prz->buffer的的数据转存到申请的prz->old_log里面。其中prz->buffer->data保存的reserve内存的内容,就是1m内存的一个zone,这部分内存在系统热启动之后数据不会消失,所以用来保存上次console的输出(dmesg内容)。而prz->old_log是通过kmalloc申请的内存,因为pstore是基于内存的文件系统,每个console_pstore文件的数据都是在内存里面,也就是这个old_log里面。
persistent_ram_save_old函数在系统启动事后会调用两次,两次调用栈分别如下:
[ 1.489784] CPU: 0 PID: 1 Comm: swapper/0 Not tainted 3.10.33 #95
[ 1.495852] [] (unwind_backtrace+0x0/0xec) from [] (show_stack+0x10/0x14)
[ 1.504445] [] (show_stack+0x10/0x14) from [] (persistent_ram_save_old+0x1a0/0x1e0)
[ 1.513926] [] (persistent_ram_save_old+0x1a0/0x1e0) from [] (persistent_ram_new+0x3d0/0x440)
[ 1.524281] [] (persistent_ram_new+0x3d0/0x440) from [] (ramoops_init_prz.constprop.2+0x88/0xf0)
[ 1.534886] [] (ramoops_init_prz.constprop.2+0x88/0xf0) from [] (ramoops_probe+0x294/0x50c)
[ 1.545062] [] (ramoops_probe+0x294/0x50c) from [] (really_probe+0xbc/0x1e8)
[ 1.553944] [] (really_probe+0xbc/0x1e8) from [] (__driver_attach+0x74/0x98)
[ 1.562827] [] (__driver_attach+0x74/0x98) from [] (bus_for_each_dev+0x4c/0xa0)
[ 1.571970] [] (bus_for_each_dev+0x4c/0xa0) from [] (bus_add_driver+0xd0/0x25c)
[ 1.581113] [] (bus_add_driver+0xd0/0x25c) from [] (driver_register+0xa8/0x140)
[ 1.590263] [] (driver_register+0xa8/0x140) from [] (ramoops_init+0x104/0x110)
[ 1.599296] [] (ramoops_init+0x104/0x110) from [] (do_one_initcall+0xa0/0x148)
[ 1.608373] [] (do_one_initcall+0xa0/0x148) from [] (kernel_init_freeable+0x190/0x23c)
[ 1.618116] [] (kernel_init_freeable+0x190/0x23c) from [] (kernel_init+0xc/0x160)
[ 1.627433] [] (kernel_init+0xc/0x160) from [] (ret_from_fork+0x14/0x3c)
-------------------------------------------------------------------------------------------------------------------------------------------------------------------
[ 61.779698] [] (unwind_backtrace+0x0/0xec) from [] (show_stack+0x10/0x14)
[ 61.788033] [] (show_stack+0x10/0x14) from [] (persistent_ram_save_old+0x1a0/0x1e0)
[ 61.797918] [] (persistent_ram_save_old+0x1a0/0x1e0) from [] (ramoops_pstore_read+0x8c/0x19c)
[ 61.807944] [] (ramoops_pstore_read+0x8c/0x19c) from [] (pstore_get_records+0xf0/0x148)
[ 61.817731] [] (pstore_get_records+0xf0/0x148) from [] (pstore_fill_super+0xa8/0xbc)
[ 61.827301] [] (pstore_fill_super+0xa8/0xbc) from [] (mount_single+0x58/0xd0)
[ 61.836341] [] (mount_single+0x58/0xd0) from [] (mount_fs+0x70/0x170)
[ 61.844591] [] (mount_fs+0x70/0x170) from [] (vfs_kern_mount+0x4c/0xcc)
[ 61.853029] [] (vfs_kern_mount+0x4c/0xcc) from [] (do_mount+0x79c/0x8c4)
[ 61.861549] [] (do_mount+0x79c/0x8c4) from [] (SyS_mount+0x84/0xb8)
[ 61.869668] [] (SyS_mount+0x84/0xb8) from [] (ret_fast_syscall+0x0/0x30)
第一次,在kenel_init的时候,系统初始化各个驱动,当初始化到ramoops驱动的时候,driver_register注册平台驱动的driver通过bus找到对应的platform device从而驱动和设备匹配成功,导致驱动的probe函数被调用,
probe里面初始化每个zone的时候从新格式化reserve内存,但如果发现上次的buffer->data的数据是有效的 ,就不进行格式化,使用原来的旧的zone。
如果发现原来的数据是有效的,调用persistent_ram_save_old函数将buffer->data里面的数据拷贝到old_log里面。
第二次,在脚本mount pstore文件系统到/sys/fs/pstore目录的时候,这时候如果发现buffer->data里有数据,还会把buffer->data的数据拷贝到old_log里面,并且在/sys/fs/pstore目录下为每个prz zone生成一个文件(console_ramoops* 和dmesg_ramoops*这些),每个文件使用对应的prz
的old_log来存储文件内容。
<4> crash触发发生的过程
第一调用persistent_ram_save_old时候,prz->old_log为NULL,kmalloc 为其申请内存,申请size大小的内存,这个size是buffer->data里面数据的长度,而buffer->data所在缓存区的长度为16368(4个页减去persistent_ram_buffer结构体的大小)。也就是说如果buffer里面数据是1000的话那么
kmalloc就为其申请1024字节的大小(2 power对齐)的slub内存,但此时buffer的最大容量是16368。
第二次调用persistent_ram_save_old 函数的时候,因为prz->old_log已经申请内存所以使用原来的1024的kmalloc的slub内存。但此时的对于console pstore来说,在mount pstore文件系统的时候系统已经完成初始化,dmesg 信息已经很多,console pstore是实时保存dmesg里面的信息,buffer的里的数据已经满了,buffer->data里面的数据是16368大小,persistent_ram_save_old函数最后两个memcpy会将16368字节的内存拷贝到1024大小的kmalloc的内存里,造成越界写。
<5 >必现方法
了解crash的过程之后,可以找到几乎是必现的方法。
1.修改/app/system/miner.plugin-crashreporter.ipk/bin/ramoops.sh脚本,只保留mount -t pstore - /sys/fs/pstore。
2.重启机器,使脚本生效。
3.删除/sys/fs/pstore下的console-ramoops文件,马上重启机器。
4.在重启的过程中几乎必现此bug。
删除pstore文件时候,在rm系统调用里会调用persistent_ram_free_old函数会释放prz->old_log并调用persistent_ram_zap将buffer的有效数据size清零。删除之后马上重启机器可保证重启之后第一次为old_log申请内存的时候size小于buffer_szie,即数据未满,在第二次调用persistent_ram_save_old函数的时候,buffer中有效数据size肯定为满,此时memcpy肯定会发生越界,就会复现。
<6>修正
memcpy写越界主要的是因为,在小的内存copy长的数据,没有考虑到pstore console中的数据是满之前持续增长的。
所以修正也比较简单,就是在第一次为old_log申请kmalloc内存的时候,size大小应该比照缓存区最大大小(16368)来分配,而不是比照有效数据来分配。