segfault at 7fff6d99febc ip 0000003688644323 sp 00007fff6d99fd30 error 7 in libc.so.6[3688600000+175000]

简述:

  进程信号11段错误,出core。内核dmesg打印:*****(程序名字)[31255]: segfault at 7fff6d99febc ip 0000003688644323 sp 00007fff6d99fd30 error 7 in libc.so.6[3688600000+175000]

直接原因:

  sp指针向下增长时,越界,访问到mmap以只读映射的地址空间,导致写操作异常。do_page_fault失败,给进程发了信号11。

环境:

  root ~ # uname -a
  Linux localhost 2.6.30-gentoo-r8 #47 SMP Mon Sep 21 03:44:07 EDT 2015 x86_64 GNU/Linux

根本原因:

  进程地址空间大致如下分布如下(我的环境是这个分布):

  (图片摘自《深入Linux内核架构》)

  与问题相关的关键点

  ①stack向下增长,所以系统必须给stack留下足够的地址空间。位于stack下面的是受限于mm->mmap_base的mmap地址空间。

  ②mm->mmap_base表示用于mmap的地址空间的最大值,查找方向向下,所有的mmap系统调用分配出来的地址空间位于mm->mmap_base之下。

  综合①和②,可以得到结论mm->mmap_base必须小于并远离Stack。但是2.6内核这里存在bug,不能一定满足该条件。

  mmap_base赋值分析:

  mm->mmap_base赋值在load_elf_binary()->arch_pick_mmap_layout()->mmap_base()中:

static unsigned long mmap_base(void)
{
	unsigned long gap = current->signal->rlim[RLIMIT_STACK].rlim_cur;//改值为栈大小限制,系统可配,默认1M。问题环境改值为8M

	if (gap < MIN_GAP)
		gap = MIN_GAP;   //走这个逻辑,MIN_GAP值为128M,gap值更新为128M。
	else if (gap > MAX_GAP)
		gap = MAX_GAP;
	//7FFFFFFFF000(TASK_SIZE) - 8000000(gap)
	//[0,FFFFFFF000], mmap_rnd()的取值范围区间
	//[7EFFF8000000,7FFFF7FFF000],返回的mmap_base的地址的取值范围
	return PAGE_ALIGN(TASK_SIZE - gap - mmap_rnd());
}

  从代码中看,mmap_base赋值中只考虑了当前栈大小,但并不能保证远离stack。

  极端情况下,mmap_rnd随机值为0时,mmap_base被赋值为TASK_SIZE-128M的位置,即:7FFFF7FFF000

  栈底赋值分析:

  Stack的栈底为:STACK_TOP - random_variable,经计算取值范围为:[7FFC00000000,7FFFFFFFF000]

#define STACK_TOP TASK_SIZE

retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),
				 executable_stack);

static unsigned long randomize_stack_top(unsigned long stack_top)
{
	unsigned int random_variable = 0;

	if ((current->flags & PF_RANDOMIZE) &&
		!(current->personality & ADDR_NO_RANDOMIZE)) {
		random_variable = get_random_int() & STACK_RND_MASK; //[0, 3fffff000]
		random_variable <<= PAGE_SHIFT;
	}
#ifdef CONFIG_STACK_GROWSUP
	return PAGE_ALIGN(stack_top) + random_variable;
#else
	return PAGE_ALIGN(stack_top) - random_variable; //[7FFC00000000,7FFFFFFFF000]
#endif
}

  

  综上,这里确认2.6的内核存在bug。改动方法是,给mmap_base至少再减去 STACK_RND_MASK。

  该问题内核最早于2009年修复:

  https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/arch/x86/mm/mmap.c?id=80938332d8cf652f6b16e0788cf0ca136befe0b5

  新版本的内核实现:

  https://code.woboq.org/linux/linux/arch/x86/mm/mmap.c.html#mmap_base

问题排查过程:

  ①充分分析core文件,确认栈的增长是正常的。导致异常的地址,也确实是正常的。函数调用时,参数正常压栈的地址。

  ②怀疑内核问题(缺页中断失败?)。找到全部打印 segfault at 的内核代码,逐个分析,对可能的路径添加调试。确认错误原因在,do_page_fault的1130行,判断vm是否可写时失败。

  ③正常来说,这个vm是stack的vm_area_struct结构体,一定是有写权限的。怀疑vm的问题,增加调试,在出问题时,打印vm_area_struct的信息。根据vma中的file指针找到被mmap的文件的名字(我的环境是个字符设备)。

  至此确认问题原因:栈的增长踩到了字符设备的mmap地址空间,该mmap是只读。调试代码如下:

/*
 * This routine handles page faults.  It determines the address,
 * and the problem, and then passes it off to one of the appropriate
 * routines.
 */
dotraplinkage void __kprobes
do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
    struct vm_area_struct *vma;
    struct task_struct *tsk;
    unsigned long address;
    struct mm_struct *mm;
    int write;
    int fault;

    tsk = current;
    mm = tsk->mm;

    prefetchw(&mm->mmap_sem);

    /* Get the faulting address: */
    address = read_cr2();
...
    if (unlikely(expand_stack(vma, address))) {
        bad_area(regs, error_code, address);
        return;
    }

    /*
     * Ok, we have a good vm_area for this memory access, so
     * we can handle it..
     */
good_area:
    write = error_code & PF_WRITE;

    if (unlikely(access_error(error_code, write, vma))) {
        printk("svking vm_next info :start:%lx  end:%lx  flags:%lx
", vma->vm_next->vm_start, 
            vma->vm_next->vm_end, vma->vm_next->vm_flags);
        printk("svking vm info :start:%lx  end:%lx  flags:%lx
", vma->vm_start, vma->vm_end, vma->vm_flags);
        printk("svking vm info :vm_file: %p
", vma->vm_file);
        if (vma->vm_file)
        {
            printk("svking file name :%s, %s
", vma->vm_file->f_dentry->d_iname,
                vma->vm_file->f_dentry->d_name.name);
        }
        bad_area_access_error(regs, error_code, address);
        return;
    }

    /*
     * If for any reason at all we couldn't handle the fault,
     * make sure we exit gracefully rather than endlessly redo
     * the fault:
     */
    fault = handle_mm_fault(mm, vma, address, write);
...
}

  ④分析mmap机制,确认mmap的返回的地址空间受控于进程的mm->mmap_base。同时mm->mmap_base是在进程启动时执行exec函数时确定的。所以怀疑mm->mmap_base赋值时存在问题。

  ⑤分析进程地址空间和mmap_base赋值函数,确认根本原因: “mm->mmap_base必须小于并远离Stack” 未能满足。

附:

调试日志(***屏蔽了业务关键字):

root ~ # tail -f /var/log/messages | grep -E "svking|segfault"
Jul 21 16:44:16 localhost kernel: [ 5489.150802] svking vm_next info :start:7fff13d58000 end:7fff13d6d000 flags:100177
Jul 21 16:44:16 localhost kernel: [ 5489.150990] svking vm info :start:7ffdcb558000 end:7fff13d58000 flags:400844fd
Jul 21 16:44:16 localhost kernel: [ 5489.151155] svking vm info :vm_file: ffff880082dbbcc0
Jul 21 16:44:16 localhost kernel: [ 5489.151260] svking file name :******, ******
Jul 21 16:44:16 localhost kernel: [ 5489.151388] ******[22867]: segfault at 7fff13d57ffc ip 00007fff6469ae99 sp 00007fff13d57ff0 error 7 in lib****.so[7fff64681000+2a000]
Jul 21 16:48:42 localhost kernel: [ 5754.180545] svking vm_next info :start:7fffb5202000 end:7fffb5217000 flags:100177
Jul 21 16:48:42 localhost kernel: [ 5754.180772] svking vm info :start:7ffe6ca02000 end:7fffb5202000 flags:400844fd
Jul 21 16:48:42 localhost kernel: [ 5754.180971] svking vm info :vm_file: ffff880082f74240
Jul 21 16:48:42 localhost kernel: [ 5754.181136] svking file name :******, ******
Jul 21 16:48:42 localhost kernel: [ 5754.181277] ******[32049]: segfault at 7fffb5201ff8 ip 00007ffff5b744ae sp 00007fffb5202000 error 7

ps:个人笔记,如有错误,期待指出,不喜勿喷。

原文地址:https://www.cnblogs.com/svking/p/9429615.html