当超群发来的问题,一个低端内存耗尽导致系统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(&current->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

发表回复