从一次内存泄露看程序在内核中的执行过程
当超群发来的问题,一个低端内存耗尽导致系统panic的问题,通过写一个死循环循环调用同一个脚本,然后盯着/proc/slabinfo看,就会发现size-32类型的slab火速增加。通过简单直接有效古老的排除法(感谢每日持续集成编译),确定是内核的问题,老的内核没问题,新内核有问题,本来直接上大神器,kmemcheck,奈何看了一眼内核,版本太低,不支持。
只能用土法炼钢了,挨着排查新内核增加的补丁,直接奔着有内存分配的修改去了,终于发现了一个可疑的补丁,对应社区补丁exec-do-not-leave-bprm-interp-on-stack.patch,中有分配内存:
int bprm_change_interp(char *interp, struct linux_binprm *bprm) { /* If a binfmt changed the interp, free it first. */ if (bprm->interp != bprm->filename) kfree(bprm->interp); bprm->interp = kstrdup(interp, GFP_KERNEL); if (!bprm->interp) return -ENOMEM; return 0; }
kstrdup通过__kmalloc分配了内存,调用者有load_misc_binary和load_script,两个调用者一个是执行二进制进行加载的函数,另外一个是脚本的处理方式(内核对普通二进制和脚本进行了区分),后面的代码均以load_misc_binary为例。
首先还是要先看一下这个补丁是干啥用的,未打补丁前是
static int load_misc_binary(struct linux_binprm *bprm, struct pt_regs *regs) { char iname[BINPRM_BUF_SIZE]; const char *iname_addr = iname; ... bprm->interp = iname; /* for binfmt_script */}
load_misc_binary给了bprm下的interp一个数组作为申请空间,但实际上写补丁的人忽略了一点,iname数组随着函数的结束就是将内存空间释放了,而bprm还在,它的interp指向了一个已释放的空间。这就存在的安全隐患,任何执行程序都可能访问内核隐私信息(已释放后的重新被其他进程分配的内存信息),所以使用了bprm_change_interp重新为interp分配内存。
下面就要看一下它在哪里释放了这块申请的内存,先顺一下程序执行的过程。
do_execve调用do_execve_common,do_execve_common在此处初始化了bprm
static int do_execve_common(const char *filename, struct user_arg_ptr argv, struct user_arg_ptr envp, struct pt_regs *regs) { struct linux_binprm *bprm; bprm = kzalloc(sizeof(*bprm), GFP_KERNEL); }
最后发现bprm也是通过free_bprm(bprm)释放的
void free_bprm(struct linux_binprm *bprm) { free_arg_pages(bprm); if (bprm->cred) { mutex_unlock(¤t->signal->cred_guard_mutex); abort_creds(bprm->cred); } kfree(bprm); }
根本没有对bprm下的指针动态申请的空间做处理,翻看补丁,也是一样没有提供(我司专有补丁和社区链接不一致,由SUSE专门提供给低版本内核,前面链接的补丁是有处理的)。原因就基本清楚了,bprm结构体下申请空间的指针没有释放处理,程序每执行一次就会申请一块内存不释放,所以重现的时候死循环调用空脚本,就会使明显复现。
中间的一些过程也贴一下,do_execve_common查找程序处理对象search_binary_handler:
struct linux_binfmt { struct list_head lh; struct module *module; int (*load_binary)(struct linux_binprm *, struct pt_regs * regs); int (*load_shlib)(struct file *); int (*core_dump)(struct coredump_params *cprm); unsigned long min_coredump; /* minimal dump size */}; int search_binary_handler(struct linux_binprm *bprm,struct pt_regs *regs) { struct linux_binfmt *fmt; ..... int (*fn)(struct linux_binprm *, struct pt_regs *) = fmt->load_binary; }
search_binary_handler通过linux_binfmt的load_binary处理不同程序,fmt初始化在
static struct linux_binfmt misc_format = { .module = THIS_MODULE, .load_binary = load_misc_binary, };
load_binary被定义成load_misc_binary,如此整个过程就串起来了,load_script的处理过程是基本相同的,可以自行看代码。
从一次内存泄露看程序在内核中的执行过程来自于OenHan
链接为:https://oenhan.com/kernel-program-exec