内核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 : [<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内存越界排查过程相关推荐
- 转:记一次linux oom内存溢出排查过程
@转:记一次linux oom内存溢出排查过程 记一次linux oom内存溢出排查过程 2018年08月16日 14:13:49 enchanterblue 阅读数 4099更多 分类专栏: --- ...
- 记一次SOFA内存泄漏排查过程
记一次内存泄漏排查过程 起因 某天中午大家还在安静的午休,睡得正香的时候突然被一阵手机滴-滴滴直响短信惊醒.一看是应用的服务器告警并且对应服务的所有机器都在告警"健康检查失败,自动拉下线&q ...
- 记一次linux oom内存溢出排查过程
一,背景 收到应用服务报警,然后登录上服务器查看原因,发现进程不再了. 二,问题分析 1,那么判断进程被干掉的原因如下: (1),机器重启了 通过uptime看机器并未重启 (2),程序有bug自动退 ...
- 一次 Java 内存泄漏排查过程,学习学习
人人都会犯错,但一些错误是如此的荒谬,我想不通怎么会有人犯这种错误.更没想到的是,这种事竟发生在了我们身上.当然,这种东西只有事后才能发现真相.接下来,我将讲述一系列最近在我们一个应用上犯过的这种错误 ...
- 一次 Java 内存泄漏排查过程,涨姿势
人人都会犯错,但一些错误是如此的荒谬,我想不通怎么会有人犯这种错误.更没想到的是,这种事竟发生在了我们身上.当然,这种东西只有事后才能发现真相.接下来,我将讲述一系列最近在我们一个应用上犯过的这种错误 ...
- 【Linux 内核 内存管理】内存管理架构 ④ ( 内存分配系统调用过程 | 用户层 malloc free | 系统调用层 brk mmap | 内核层 kmalloc | 内存管理流程 )
文章目录 一.内存分配系统调用过程 ( 用户层 | 系统调用 | 内核层 ) 二.内存管理流程 一.内存分配系统调用过程 ( 用户层 | 系统调用 | 内核层 ) " 堆内存 " ...
- Linux 基础知识(2)---Linux内核空间内存申请函数kmalloc、kzalloc、vmalloc的区别
Linux内核空间内存申请函数kmalloc.kzalloc.vmalloc的区别 kzalloc与kmalloc区别 这个函数就是原来的两个函数的整合 , 即原来我们每次申请内存的时候都会这么 ...
- 一次堆外内存泄露的排查过程
转载自 一次堆外内存泄露的排查过程 最近在做一个基于 websocket 的长连中间件,服务端使用实现了 socket.io 协议(基于websocket协议,提供长轮询降级能力) 的 netty- ...
- 内存很空却频繁gc_记一次不太成功的频繁 full gc 排查过程
上周自己负责的一个应用出现频繁full gc的问题,不得不尝试优化一下.第一次做这种事只能先看看网上的文章,然后亲自尝试怎么去完成减少full gc的频率,降低young gc的频率这一目标.虽然最终 ...
最新文章
- JAVA多线程和并发基础面试问答
- pe常用软件_验证U盘PE系统,有几款纯净好用
- java tomcat日志中文乱码问题解决
- 在ASP.NET Core上实施每个租户策略的数据库
- c语言程序中的if-else语句,C语言if else语句
- vue生命周期,vue执行顺序图,钩子函数
- mysql yum安装包下载_yum 下载安装包
- Informix日志报错:Could not do a physical-order read to fetch netxt row
- 用java代码将从数据库中取出的具有父子关系的数据转成json格式
- tps协议和onvif协议_ONVIF协议解读
- web前端入门到实战:CSS3中width值为max/min-content及fit-content的理解
- LeetCode | 347. Top K Frequent Elements
- html常用标签和属性
- 2022P气瓶充装考试试题及在线模拟考试
- Python:实现骰子游戏
- 《强化学习周刊》第42期:DPIN、鲁棒元强化学习、Deep dispatching
- 如何熟练掌握运用Delft3D建模、水动力模拟方法及在地表水环境影响评价中的实践技术
- 员工评价系统第一天,项目需求分析
- 双极权电阻网络串联电阻阻值推导
- macOS下妙用option按键
热门文章
- 帝国cms安装不支持MySQL_15招完美解决帝国CMS安装出错和常见问题
- 中国机器人安装量创历史新高;租车巨头赫兹将向优步提供5万辆特斯拉 | 美通社头条...
- Idea 设置编码UTF-8 Idea中 .properties 配置文件中文乱码
- 中山纪中集训游记Day2+8.2模拟赛题解
- 训练自己集中注意力就是要驯服这只大猴子
- 第二个设备 绝缘、耐压
- 从Sigmoid到GELU,一文概览神经网络激活函数
- Redis分布式Session
- 英文论文(sci)解读复现【NO.4】FINet:基于合成雾和改进YOLOv5的绝缘子数据集和检测基准(代码已复现)
- Android 源码 Camera2 获取 CameraId 列表