欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

munmap参数错误导致进程空间异常

程序员文章站 2022-07-14 18:25:30
...

去年在现场遇到一个问题,使用mmap操作文件的时候,总是莫名奇妙的core,用gdb 命令info file查看进程空间,发现stack栈空间非常大,觉得莫名其妙。后来发现是munmap传入的len参数错误,导致系统删除了不该删除的内存。一直以为kernel会帮我校验地址空间是否合法,所以觉得奇怪,不过一直到这几天,才拿出来代码看了看。原来内核不会校验[addr,addr+len]范围是否合法,也不管中间是否有空洞,只是尽量删除更多的区域(vma)。

代码是3.13的,文件是mm/mmap.c

SYSCALL_DEFINE2(munmap, unsigned long, addr, size_t, len)
/* Munmap is split into 2 main parts -- this part which finds
 * what needs doing, and the areas themselves, which do the
 * work.  This now handles partial unmappings.
 * Jeremy Fitzhardinge <[email protected]>
 */
int do_munmap(struct mm_struct *mm, unsigned long start, size_t len)
{
    unsigned long end;
    struct vm_area_struct *vma, *prev, *last;

    // start已经要页对齐
    // [start,start+len]一定在正确的范围内,即都大于TASK_SIZE
    if ((start & ~PAGE_MASK) || start > TASK_SIZE || len > TASK_SIZE-start)
        return -EINVAL;

    // 长度不能是0
    if ((len = PAGE_ALIGN(len)) == 0)
        return -EINVAL;

    /* Find the first overlapping VMA */
    // 找到一个vma区域,他的vm_end要比start大。不过不能保证这个区域就包含了start,也可能vma.vm_start也在start后面,甚至在start+len后面
    vma = find_vma(mm, start);
    if (!vma)
        return 0;
    prev = vma->vm_prev;
    /* we have  start < vma->vm_end  */

    /* vma区域跟[start,end]没有重叠,也只有找到的这个vma可能与[start,end]重叠 */
    end = start + len;
    if (vma->vm_start >= end)
        return 0;

    // 如果vma与[start,end]重叠,就拆分vma。
    // 只有start在(vma.vm_start, vma.vm_end)范围内时才会有交叉重叠,
    // vma.vm_end肯定是比start大的,那么如果vma.vm_start比start小,
    // 就能确定两个区域有重叠,需要拆分vma。
    if (start > vma->vm_start) {
        int error;

        /*
         * Make sure that map_count on return from munmap() will
         * not exceed its limit; but let map_count go just above
         * its limit temporarily, to help free resources as expected.
         */
        if (end < vma->vm_end && mm->map_count >= sysctl_max_map_count)
            return -ENOMEM;

        error = __split_vma(mm, vma, start, 0);
        if (error)
            return error;
        prev = vma;
    }

    // end的判断也是类似的,看看包含end的vma是否需要拆分
    last = find_vma(mm, end);
    if (last && end > last->vm_start) {
        int error = __split_vma(mm, last, end, 1);
        if (error)
            return error;
    }
    vma = prev? prev->vm_next: mm->mmap;

    // 尝试解锁某些加锁区域,如果存在的话
    if (mm->locked_vm) {
        struct vm_area_struct *tmp = vma;
        while (tmp && tmp->vm_start < end) {
            if (tmp->vm_flags & VM_LOCKED) {
                mm->locked_vm -= vma_pages(tmp);
                munlock_vma_pages_all(tmp);
            }
            tmp = tmp->vm_next;
        }
    }

    // 把unmap需要删除的vma从mm中删除,并返回需要删除的vma链表
    detach_vmas_to_be_unmapped(mm, vma, prev, end);
    // 通知MMU,刷新TLB
    unmap_region(mm, vma, prev, start, end);

    // 对每个vma做vma.vm_ops.close(如果有的话),还要释放文件(vma.vm_file)
    remove_vma_list(mm, vma);

    return 0;
}

detach_vmas_to_be_unmapped这个函数负责把[start,end]范围内的vma全部从进程空间中删除,并没有检查中间的区域之间是否有空洞。所以如果传入的长度比较大,超过了之前mmap的大小,也不会出现错误,导致undefined异常。

static void
detach_vmas_to_be_unmapped(struct mm_struct *mm, struct vm_area_struct *vma,
    struct vm_area_struct *prev, unsigned long end)
{
    struct vm_area_struct **insertion_point;
    struct vm_area_struct *tail_vma = NULL;

    insertion_point = (prev ? &prev->vm_next : &mm->mmap);
    vma->vm_prev = NULL;
    do {
        vma_rb_erase(vma, &mm->mm_rb); // 从进程空间中删除所有的vma
        mm->map_count--;
        tail_vma = vma;
        vma = vma->vm_next;
    } while (vma && vma->vm_start < end);
    *insertion_point = vma;
    if (vma) {
        vma->vm_prev = prev;
        vma_gap_update(vma);
    } else
        mm->highest_vm_end = prev ? prev->vm_end : 0;
    tail_vma->vm_next = NULL;
    mm->mmap_cache = NULL;      /* Kill the cache. */
}
相关标签: linux memory