内核kmalloc内存越界排查过程

<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)来分配,而不是比照有效数据来分配。

 

你可能感兴趣的:(Linux调试)