<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 : [<c0464bfc>] lr : [<c0748898>] 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 <linux/kernel.h>2 #include <linux/module.h>3 #include <linux/kprobes.h>4 #include <linux/printk.h>5 6 #define VFM_NAME_LEN    1007 #define VFM_MAP_SIZE    108 #define VFM_MAP_COUNT   209 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] [<c0014e90>] (unwind_backtrace+0x0/0xec) from [<c0011df0>] (sh)
[ 30.608502@0] [<c0011df0>] (show_stack+0x10/0x14) from [<bf1e9054>] (my_memc)
[ 30.617298@0] [<bf1e9054>] (my_memcpy+0x54/0x64 [probe]) from [<c03d8224>] ()
[ 30.627384@0] [<c03d8224>] (persistent_ram_save_old+0x124/0x148) from [<c03d)
[ 30.637733@0] [<c03d7d38>] (ramoops_pstore_read+0x90/0x180) from [<c03d71a8>)
[ 30.647564@0] [<c03d71a8>] (pstore_get_records+0xf0/0x148) from [<c03d68d8>])
[ 30.657143@0] [<c03d68d8>] (pstore_fill_super+0xa8/0xbc) from [<c0127150>] ()
[ 30.666112@0] [<c0127150>] (mount_single+0x58/0xd0) from [<c0127298>] (mount)
[ 30.674392@0] [<c0127298>] (mount_fs+0x70/0x170) from [<c013e794>] (vfs_kern)
[ 30.682844@0] [<c013e794>] (vfs_kern_mount+0x4c/0xcc) from [<c0140e8c>] (do_)
[ 30.691379@0] [<c0140e8c>] (do_mount+0x784/0x8a8) from [<c0141034>] (SyS_mou)
[ 30.699488@0] [<c0141034>] (SyS_mount+0x84/0xb8) from [<c000dac0>] (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] [<c0014e30>] (unwind_backtrace+0x0/0xec) from [<c0011d94>] (show_stack+0x10/0x14)
[ 1.504445] [<c0011d94>] (show_stack+0x10/0x14) from [<c03d8300>] (persistent_ram_save_old+0x1a0/0x1e0)
[ 1.513926] [<c03d8300>] (persistent_ram_save_old+0x1a0/0x1e0) from [<c03d893c>] (persistent_ram_new+0x3d0/0x440)
[ 1.524281] [<c03d893c>] (persistent_ram_new+0x3d0/0x440) from [<c09ebb04>] (ramoops_init_prz.constprop.2+0x88/0xf0)
[ 1.534886] [<c09ebb04>] (ramoops_init_prz.constprop.2+0x88/0xf0) from [<c03d78a4>] (ramoops_probe+0x294/0x50c)
[ 1.545062] [<c03d78a4>] (ramoops_probe+0x294/0x50c) from [<c04bb708>] (really_probe+0xbc/0x1e8)
[ 1.553944] [<c04bb708>] (really_probe+0xbc/0x1e8) from [<c04bb900>] (__driver_attach+0x74/0x98)
[ 1.562827] [<c04bb900>] (__driver_attach+0x74/0x98) from [<c04b9c3c>] (bus_for_each_dev+0x4c/0xa0)
[ 1.571970] [<c04b9c3c>] (bus_for_each_dev+0x4c/0xa0) from [<c04bae68>] (bus_add_driver+0xd0/0x25c)
[ 1.581113] [<c04bae68>] (bus_add_driver+0xd0/0x25c) from [<c04bbf54>] (driver_register+0xa8/0x140)
[ 1.590263] [<c04bbf54>] (driver_register+0xa8/0x140) from [<c0e30508>] (ramoops_init+0x104/0x110)
[ 1.599296] [<c0e30508>] (ramoops_init+0x104/0x110) from [<c000854c>] (do_one_initcall+0xa0/0x148)
[ 1.608373] [<c000854c>] (do_one_initcall+0xa0/0x148) from [<c0e0ac68>] (kernel_init_freeable+0x190/0x23c)
[ 1.618116] [<c0e0ac68>] (kernel_init_freeable+0x190/0x23c) from [<c09e4040>] (kernel_init+0xc/0x160)
[ 1.627433] [<c09e4040>] (kernel_init+0xc/0x160) from [<c000db58>] (ret_from_fork+0x14/0x3c)-------------------------------------------------------------------------------------------------------------------------------------------------------------------[ 61.779698] [<c0014e30>] (unwind_backtrace+0x0/0xec) from [<c0011d94>] (show_stack+0x10/0x14)
[ 61.788033] [<c0011d94>] (show_stack+0x10/0x14) from [<c03d8300>] (persistent_ram_save_old+0x1a0/0x1e0)
[ 61.797918] [<c03d8300>] (persistent_ram_save_old+0x1a0/0x1e0) from [<c03d7d78>] (ramoops_pstore_read+0x8c/0x19c)
[ 61.807944] [<c03d7d78>] (ramoops_pstore_read+0x8c/0x19c) from [<c03d71ec>] (pstore_get_records+0xf0/0x148)
[ 61.817731] [<c03d71ec>] (pstore_get_records+0xf0/0x148) from [<c03d6894>] (pstore_fill_super+0xa8/0xbc)
[ 61.827301] [<c03d6894>] (pstore_fill_super+0xa8/0xbc) from [<c01270f0>] (mount_single+0x58/0xd0)
[ 61.836341] [<c01270f0>] (mount_single+0x58/0xd0) from [<c0127238>] (mount_fs+0x70/0x170)
[ 61.844591] [<c0127238>] (mount_fs+0x70/0x170) from [<c013e734>] (vfs_kern_mount+0x4c/0xcc)
[ 61.853029] [<c013e734>] (vfs_kern_mount+0x4c/0xcc) from [<c0140e44>] (do_mount+0x79c/0x8c4)
[ 61.861549] [<c0140e44>] (do_mount+0x79c/0x8c4) from [<c0140ff0>] (SyS_mount+0x84/0xb8)
[ 61.869668] [<c0140ff0>] (SyS_mount+0x84/0xb8) from [<c000dac0>] (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)来分配,而不是比照有效数据来分配。

内核kmalloc内存越界排查过程相关推荐

  1. 转:记一次linux oom内存溢出排查过程

    @转:记一次linux oom内存溢出排查过程 记一次linux oom内存溢出排查过程 2018年08月16日 14:13:49 enchanterblue 阅读数 4099更多 分类专栏: --- ...

  2. 记一次SOFA内存泄漏排查过程

    记一次内存泄漏排查过程 起因 某天中午大家还在安静的午休,睡得正香的时候突然被一阵手机滴-滴滴直响短信惊醒.一看是应用的服务器告警并且对应服务的所有机器都在告警"健康检查失败,自动拉下线&q ...

  3. 记一次linux oom内存溢出排查过程

    一,背景 收到应用服务报警,然后登录上服务器查看原因,发现进程不再了. 二,问题分析 1,那么判断进程被干掉的原因如下: (1),机器重启了 通过uptime看机器并未重启 (2),程序有bug自动退 ...

  4. 一次 Java 内存泄漏排查过程,学习学习

    人人都会犯错,但一些错误是如此的荒谬,我想不通怎么会有人犯这种错误.更没想到的是,这种事竟发生在了我们身上.当然,这种东西只有事后才能发现真相.接下来,我将讲述一系列最近在我们一个应用上犯过的这种错误 ...

  5. 一次 Java 内存泄漏排查过程,涨姿势

    人人都会犯错,但一些错误是如此的荒谬,我想不通怎么会有人犯这种错误.更没想到的是,这种事竟发生在了我们身上.当然,这种东西只有事后才能发现真相.接下来,我将讲述一系列最近在我们一个应用上犯过的这种错误 ...

  6. 【Linux 内核 内存管理】内存管理架构 ④ ( 内存分配系统调用过程 | 用户层 malloc free | 系统调用层 brk mmap | 内核层 kmalloc | 内存管理流程 )

    文章目录 一.内存分配系统调用过程 ( 用户层 | 系统调用 | 内核层 ) 二.内存管理流程 一.内存分配系统调用过程 ( 用户层 | 系统调用 | 内核层 ) " 堆内存 " ...

  7. Linux 基础知识(2)---Linux内核空间内存申请函数kmalloc、kzalloc、vmalloc的区别

    Linux内核空间内存申请函数kmalloc.kzalloc.vmalloc的区别 kzalloc与kmalloc区别    这个函数就是原来的两个函数的整合 , 即原来我们每次申请内存的时候都会这么 ...

  8. 一次堆外内存泄露的排查过程

    转载自  一次堆外内存泄露的排查过程 最近在做一个基于 websocket 的长连中间件,服务端使用实现了 socket.io 协议(基于websocket协议,提供长轮询降级能力) 的 netty- ...

  9. 内存很空却频繁gc_记一次不太成功的频繁 full gc 排查过程

    上周自己负责的一个应用出现频繁full gc的问题,不得不尝试优化一下.第一次做这种事只能先看看网上的文章,然后亲自尝试怎么去完成减少full gc的频率,降低young gc的频率这一目标.虽然最终 ...

最新文章

  1. JAVA多线程和并发基础面试问答
  2. pe常用软件_验证U盘PE系统,有几款纯净好用
  3. java tomcat日志中文乱码问题解决
  4. 在ASP.NET Core上实施每个租户策略的数据库
  5. c语言程序中的if-else语句,C语言if else语句
  6. vue生命周期,vue执行顺序图,钩子函数
  7. mysql yum安装包下载_yum 下载安装包
  8. Informix日志报错:Could not do a physical-order read to fetch netxt row
  9. 用java代码将从数据库中取出的具有父子关系的数据转成json格式
  10. tps协议和onvif协议_ONVIF协议解读
  11. web前端入门到实战:CSS3中width值为max/min-content及fit-content的理解
  12. LeetCode | 347. Top K Frequent Elements
  13. html常用标签和属性
  14. 2022P气瓶充装考试试题及在线模拟考试
  15. Python:实现骰子游戏
  16. 《强化学习周刊》第42期:DPIN、鲁棒元强化学习、Deep dispatching
  17. 如何熟练掌握运用Delft3D建模、水动力模拟方法及在地表水环境影响评价中的实践技术
  18. 员工评价系统第一天,项目需求分析
  19. 双极权电阻网络串联电阻阻值推导
  20. macOS下妙用option按键

热门文章

  1. 帝国cms安装不支持MySQL_15招完美解决帝国CMS安装出错和常见问题
  2. 中国机器人安装量创历史新高;租车巨头赫兹将向优步提供5万辆特斯拉 | 美通社头条...
  3. Idea 设置编码UTF-8 Idea中 .properties 配置文件中文乱码
  4. 中山纪中集训游记Day2+8.2模拟赛题解
  5. 训练自己集中注意力就是要驯服这只大猴子
  6. 第二个设备 绝缘、耐压
  7. 从Sigmoid到GELU,一文概览神经网络激活函数
  8. Redis分布式Session
  9. 英文论文(sci)解读复现【NO.4】FINet:基于合成雾和改进YOLOv5的绝缘子数据集和检测基准(代码已复现)
  10. Android 源码 Camera2 获取 CameraId 列表