Linux Kernel get_user_pages() 源码分析

前言

最近在看linux内核,然后也边看边分析写kernel CVE,这两天在分析复现CVE-2016-5195(dirty cow),感觉这个漏洞已经被大家分析烂了,但是还是很有学习的价值,自己分析一遍,还是很有收获,具体的漏洞分析就不写了,这篇文件记录我在分析这个漏洞的时候阅读的相关源码的一些记录。
linux源码版本基于linux-3.3-rc7。

当文件被加载到系统中之后会创建虚拟区间完成地址映射,但是不会将文件的数据拷贝到内存,直到文件数据被访问,才会将相应物理页拷贝到内存。当进程第一次访问该read-only文件,由于对应的物理页不在内存,会引发缺页异常。
write系统调用可用于往内存区域写数据。具体的函数调用流程就不写了,网上一大堆,反正就是要往某个内存写数据,就需要知道这块内存的位置,用户只有虚拟地址,那么就需要找到虚拟地址对应的物理页,这个操作通过get_user_pages函数实现。

get_user_pages()函数分析

get_user_pages函数通过参数中的虚拟地址去请求对应的物理页。
具体分析如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int get_user_pages(struct task_struct *tsk, struct mm_struct *mm,
unsigned long start, int nr_pages, int write, int force,
struct page **pages, struct vm_area_struct **vmas)
{
int flags = FOLL_TOUCH;

if (pages)
flags |= FOLL_GET;
if (write)
flags |= FOLL_WRITE;
if (force)
flags |= FOLL_FORCE;

return __get_user_pages(tsk, mm, start, nr_pages, flags, pages, vmas,
NULL);
}

get_user_pages参数说明

  • tsk : 指向进程的进程描述符
  • mm : 指向进程的内存描述符
  • start : 要访问的起始虚拟地址
  • nr_pages : 请求页的数量
  • write : 是否以进行写操作
  • force : 是否进行读写操作
  • page : 用于保存请求物理页的页描述符
  • vmas : 指向每个页面对于的vma的指针数组

根据参数设置相应的标志位,调用__get_user_pages,继续跟进__get_user_pages函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
int __get_user_pages(struct task_struct *tsk, struct mm_struct *mm,
unsigned long start, int nr_pages, unsigned int gup_flags,
struct page **pages, struct vm_area_struct **vmas,
int *nonblocking)
......
//设置标志
vm_flags = (gup_flags & FOLL_WRITE) ?
(VM_WRITE | VM_MAYWRITE) : (VM_READ | VM_MAYREAD);
vm_flags &= (gup_flags & FOLL_FORCE) ?
(VM_MAYREAD | VM_MAYWRITE) : (VM_READ | VM_WRITE)
......
do {
struct page *page;
unsigned int foll_flags = gup_flags;
......
//进行线程调度,会将当前线程挂起
cond_resched();
//follw_page() 函数请求内存页,如果内存中已经存在该页,则返回该页的页描述符
//否则产生缺页中断,进入循环进行缺页处理,直到请求到内存页为止.
while (!(page = follow_page(vma, start, foll_flags))) {
int ret;
unsigned int fault_flags = 0;
......
//如果请求的内存页有写标志,设置写标记
if (foll_flags & FOLL_WRITE)
fault_flags |= FAULT_FLAG_WRITE;
if (nonblocking)
fault_flags |= FAULT_FLAG_ALLOW_RETRY;
if (foll_flags & FOLL_NOWAIT)
fault_flags |= (FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_RETRY_NOWAIT);

//进行缺页处理
ret = handle_mm_fault(mm, vma, start,
fault_flags);

......
//去掉FOLL_WRITE标志,这个条件满足,说明触发了COW操作,
if ((ret & VM_FAULT_WRITE) &&
!(vma->vm_flags & VM_WRITE))
foll_flags &= ~FOLL_WRITE;
//进行线程调度
cond_resched();
}
} while (nr_pages && start < vma->vm_end);
} while (nr_pages);
return i;
}
EXPORT_SYMBOL(__get_user_pages);

这里有两个关键函数follow_pagehandle_mm_fault函数。

follow_page 函数分析

follw_page 函数请求内存页,如果内存中已经存在该页,则返回该页的页描述符,否则返回null。
具体源码分析如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
struct page *follow_page(struct vm_area_struct *vma, unsigned long address,
unsigned int flags)
{
......
page = NULL;
pgd = pgd_offset(mm, address);//获取页全局目录
if (pgd_none(*pgd) || unlikely(pgd_bad(*pgd)))
goto no_page_table;
//获取页上级目录
pud = pud_offset(pgd, address);
if (pud_none(*pud))//第一次访问该虚拟地址,还没建立页目录,直接返回
goto no_page_table;
......
pmd = pmd_offset(pud, address);//获取页表,同上进行判断是否已经对于页表已经建立
if (pmd_none(*pmd))
goto no_page_table;
......
split_fallthrough:
//获取页表对应的pte
ptep = pte_offset_map_lock(mm, pmd, address, &ptl);
//得到pte内容
pte = *pte;
//如果当前页不在内存,返回null
if (!pte_present(pte))
goto no_page;
//如果请求写,但是对应页为只读,返回null
if ((flags & FOLL_WRITE) && !pte_write(pte))
goto unlock;
//走到这里说明对应页已经在内存,并且访问权限匹配,获取page
page = vm_normal_page(vma, address, pte);
......

__get_user_pages函数中,会循环调用follow_page函数请求物理页,直到成功获取到物理页,则退出循环。当第一次请求对应物理页时,该物理页没有加载到内存,会调用handle_mm_fault函数进行缺页处理。

handle_mm_fault 函数分析

handle_mm_fault函数为触发缺页的address分配各级页目录,也即有了address对应的pte了,但是此时这个pte还是没有映射到对应的物理页,这时需要调用handle_pte_fault函数将pte指向对应的物理页。

handle_pte_fault 函数分析

handle_pte_fault函数完成pte到物理页的映射。会根据不同的状态的进行不同的处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
int handle_pte_fault(struct mm_struct *mm,
struct vm_area_struct *vma, unsigned long address,
pte_t *pte, pmd_t *pmd, unsigned int flags)
{
pte_t entry;
spinlock_t *ptl;
//获取页表条目的值
entry = *pte;
//如果该entry标识的页不在内存
if (!pte_present(entry)) {
if (pte_none(entry)) {//并且entry为空,表示该页还没有被载入内存
if (vma->vm_ops) {//如果vm_ops和fault都不为空的话,表示这是基于文件的映射
if (likely(vma->vm_ops->fault))
return do_linear_fault(mm, vma, address,
pte, pmd, flags, entry);
}//否则分配匿名页
return do_anonymous_page(mm, vma, address,
pte, pmd, flags);
}//属于非线性文件映射且已被换出
if (pte_file(entry))
return do_nonlinear_fault(mm, vma, address,
pte, pmd, flags, entry);
/*如果页不在内存,但是之前pte已经映射过页,但是被换出了,则进行换入操作*/
return do_swap_page(mm, vma, address,
pte, pmd, flags, entry);
}
//缺页处理,第二次走到这里
//走到这里说明访问页在内存中,首先获取页表锁
ptl = pte_lockptr(mm, pmd);
spin_lock(ptl);
//检查当前pte的值与之前是否一致,用于检查竞争
if (unlikely(!pte_same(*pte, entry)))
goto unlock;
//该缺页异常是由于写访问触发
if (flags & FAULT_FLAG_WRITE) {
if (!pte_write(entry))//并且对应物理页的权限为只读,需要触发COW操作
return do_wp_page(mm, vma, address,
pte, pmd, ptl, entry);
entry = pte_mkdirty(entry);
}
//这是页已经在内存,并且请求访问的读写权限与物理页的读写权限匹配
//设置pte的ACCESS标志位,表示该页为访问过
entry = pte_mkyoung(entry);
if (ptep_set_access_flags(vma, address, pte, entry, flags & FAULT_FLAG_WRITE)) {
update_mmu_cache(vma, address, pte);
} else {
/*
* This is needed only for protection faults but the arch code
* is not yet telling us if this is a protection fault or not.
* This still avoids useless tlb flushes for .text page faults
* with threads.
*/
if (flags & FAULT_FLAG_WRITE)
flush_tlb_fix_spurious_fault(vma, address);
}
unlock:
pte_unmap_unlock(pte, ptl);
return 0;
}

主要进行如下操作:

  1. 如果对应的物理页不在内存,
    1. 并且pte的内容为0,表示该页还没有加到内存中
      1. 然后判断是否属于文件映射,,如果是调用do_linear_fault()分配映射页并返回。
      2. 否则分配匿名页,匿名映射一般是堆栈页,调用do_anonymous_page()函数处理并返回
    2. 如果 pte不为0,则表示之前载入过页,但是被换出,并且属于非线性文件映射,调用do_nonlinear_fault()函数处理并返回。
  2. 如果对应物理页在内存,判断请求访问标志与物理页的访问标志,如果请求写,但是对于页为只读,触发COW操作,将调用do_wp_page()函数进行处理。
  3. 如果对应物理页在内存并且访问权限匹配,则设置pte的ACCESS标志位,表示该页为访问过。

__do_fault 函数

do_linear_fault函数会调用__do_fault进行处理,具体分析如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
static int __do_fault(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pmd_t *pmd,
pgoff_t pgoff, unsigned int flags, pte_t orig_pte)
{
......
/*
* 如果是以写访问,并且当前vma属于私有映射,即触发cow
*/
if ((flags & FAULT_FLAG_WRITE) && !(vma->vm_flags & VM_SHARED)) {
//创建anon_vma实例给vma
if (unlikely(anon_vma_prepare(vma)))
return VM_FAULT_OOM;
//分配一个新页用于写
cow_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, address);
......
vmf.virtual_address = (void __user *)(address & PAGE_MASK);
vmf.pgoff = pgoff;
vmf.flags = flags;
vmf.page = NULL;
//将文件数据读入到映射页
ret = vma->vm_ops->fault(vma, &vmf);
......
page = vmf.page;
if (flags & FAULT_FLAG_WRITE) {//写访问
if (!(vma->vm_flags & VM_SHARED)) {//私有映射
page = cow_page;//将之前分配的用于cow的页赋给page
anon = 1;//标记匿名页
//将创建数据的副本,将数据拷贝到page(新分配的页)
copy_user_highpage(page, vmf.page, address, vma);
__SetPageUptodate(page);
}
......
page_table = pte_offset_map_lock(mm, pmd, address, &ptl);
......
if (likely(pte_same(*page_table, orig_pte))) {//确定没有竞争,当前的pte和之前的pte内容保持一致
flush_icache_page(vma, page);
//将页表项指向相应物理页
entry = mk_pte(page, vma->vm_page_prot);
if (flags & FAULT_FLAG_WRITE)//写访问
//如果设置了VM_WRITE,也即vma映射为可写,则将页的权限为R/W,并标记为dirty
//否则只是将该页标记为dirty,当前页还是只读
entry = maybe_mkwrite(pte_mkdirty(entry), vma);
if (anon) {//如果是匿名页
//创建匿名页与第一个vma的逆向映射
inc_mm_counter_fast(mm, MM_ANONPAGES);
page_add_new_anon_rmap(page, vma, address);
} else {//建立物理页与vma的普通映射
inc_mm_counter_fast(mm, MM_FILEPAGES);
page_add_file_rmap(page);
if (flags & FAULT_FLAG_WRITE) {
dirty_page = page;
get_page(dirty_page);
}
}
//使page_table的内容为entry对应的页框
//将page_table指向的pte以该值进行填充,这样就完成了页表项到物理页的映射
set_pte_at(mm, address, page_table, entry);
......
pte_unmap_unlock(page_table, ptl);
......

do_wp_page 函数

如果以写访问去请求访问只读页,会触发COW操作,具体由do_wp_page函数实现。具体分析如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
static int do_wp_page(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pte_t *page_table, pmd_t *pmd,
spinlock_t *ptl, pte_t orig_pte)
__releases(ptl)
{
......
//获取需要进行COW的共享页
old_page = vm_normal_page(vma, address, orig_pte);
if (!old_page) {//获取共享页失败
//如果vma本身就是共享可写的,跳转到reuse直接使用原来的
if ((vma->vm_flags & (VM_WRITE|VM_SHARED)) ==
(VM_WRITE|VM_SHARED))
goto reuse;
goto gotten;
}
//首先判断匿名页,如果当前匿名页只有一个进程引用则直接使用该页
if (PageAnon(old_page) && !PageKsm(old_page)) {
//判断是否有其他进程竞争,会导致修改页表
if (!trylock_page(old_page)) {
page_cache_get(old_page);
pte_unmap_unlock(page_table, ptl);
lock_page(old_page);
page_table = pte_offset_map_lock(mm, pmd, address,
&ptl);
if (!pte_same(*page_table, orig_pte)) {
unlock_page(old_page);
goto unlock;
}
page_cache_release(old_page);
}
//没有竞争,则判断 old_page的_mapcount字段是否为0
if (reuse_swap_page(old_page)) {

//将old_page 映射到自己的anon_vma
page_move_anon_rmap(old_page, vma, address);
unlock_page(old_page);
goto reuse;
}
unlock_page(old_page);
} else if (unlikely((vma->vm_flags & (VM_WRITE|VM_SHARED)) == //如果vma本身是共享可写的
(VM_WRITE|VM_SHARED))) {
......
page_cache_get(old_page);//增加old page 的引用计数
pte_unmap_unlock(page_table, ptl);
//修改页的权限
tmp = vma->vm_ops->page_mkwrite(vma, &vmf);
......

//走到这里表示已经成功修改了页的权限了,这里同样重新获取页表,判断是否和之前一致
page_table = pte_offset_map_lock(mm, pmd, address,
&ptl);
if (!pte_same(*page_table, orig_pte)) {
unlock_page(old_page);
goto unlock;
}
page_mkwrite = 1;
}
dirty_page = old_page;
get_page(dirty_page);

reuse: //走到这里说明不进行COW操作,直接在old page进行操作
flush_cache_page(vma, address, pte_pfn(orig_pte));
entry = pte_mkyoung(orig_pte);//标记_PAGE_ACCESSED位
//如果vma映射为可写,也即VM_ WRITE被置位,将页权限改为R/W,同时标记为dirty
//否则只将该页标记dirty, 该页还是只读
entry = maybe_mkwrite(pte_mkdirty(entry), vma); if (ptep_set_access_flags(vma, address, page_table, entry,1))
update_mmu_cache(vma, address, page_table);
pte_unmap_unlock(page_table, ptl);
ret |= VM_FAULT_WRITE;

if (!dirty_page)
return ret;
......
/*
* 走在这里说明必须要进行COW操作,要分配一个新页,
*/
page_cache_get(old_page);
gotten:
......
if (is_zero_pfn(pte_pfn(orig_pte))) {
new_page = alloc_zeroed_user_highpage_movable(vma, address);//分配一个0页面
if (!new_page)
goto oom;
} else {
new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, address);//分配一个非0的页面
if (!new_page)
goto oom;
cow_user_page(new_page, old_page, address, vma);//将old page中的数据拷贝到new page
}
__SetPageUptodate(new_page);

if (mem_cgroup_newpage_charge(new_page, mm, GFP_KERNEL))
goto oom_free_new;

/*
* Re-check the pte - we dropped the lock
*/
page_table = pte_offset_map_lock(mm, pmd, address, &ptl);
if (likely(pte_same(*page_table, orig_pte))) {
if (old_page) {
if (!PageAnon(old_page)) {
dec_mm_counter_fast(mm, MM_FILEPAGES);
inc_mm_counter_fast(mm, MM_ANONPAGES);
}
} else
inc_mm_counter_fast(mm, MM_ANONPAGES);
flush_cache_page(vma, address, pte_pfn(orig_pte));
entry = mk_pte(new_page, vma->vm_page_prot);//获取new page 的pte
//如果vma映射为可写,也即VM_ WRITE被置位,将页权限改为R/W,同时标记为dirty
//否则只将该页标记dirty, 该页还是只读
entry = maybe_mkwrite(pte_mkdirty(entry), vma);
......
page_add_new_anon_rmap(new_page, vma, address);//创建new page 到anon_vam的逆向映射

set_pte_at_notify(mm, address, page_table, entry);//更新entry对应的物理页为new page
update_mmu_cache(vma, address, page_table;//更新mmu 缓存
//释放old page
new_page = old_page;
//返回VM_FAULT_WRITE标记
ret |= VM_FAULT_WRITE;
}
......
}

Something Confusing Me

对于maybe_mkwrite函数的理解

之前分析缺页处理的时候,对这个函数只是粗略的看了一下,以为是只要调用这个函数就会将物理页的权限改写为可写。但其实是有前提的,如果当前vma映射为可写,才会调用pte_mkwrite将物理页标记为可写,否则不进行任何操作。

1
2
3
4
5
6
7
8
9
10
11
12
 * Do pte_mkwrite, but only if the vma says VM_WRITE.  We do this when
* servicing faults for write access. In the normal case, do always want
* pte_mkwrite. But get_user_pages can cause write faults for mappings
* that do not have writing enabled, when used by access_process_vm.
*/
static inline pte_t maybe_mkwrite(pte_t pte, struct vm_area_struct *vma)
{
if (likely(vma->vm_flags & VM_WRITE))
pte = pte_mkwrite(pte);
return pte;
}
#endi

CVE-2016-5195(dirty cow)条件竞争处的理解

对于一个以MAP_PRIVATE映射到内存的文件,在向该文件处写出数据时,会触发COW操作。即实际的数据会写到新分配的物理页上,不会写回到原来的文件。可能看到了几天的源码,再加上最近心情总是大起大落,把自己给绕进去了。这里详细写下为什么发生条件竞争后,可以改写只读文件。

前置条件

  1. 以只读模式打开文件A,返回文件表示符fd,将文件映射到内存,标记为MAP_PRIVATE。

正常访问过程分析

当我们通过write系统调用往文件A写数据时,第一次访问数据时,会通过虚拟地址获取到对应的物理页,由于系统只会在数据被真正访问到时才会将其对应物理页加载进内存,所以第一次访问文件,会触发缺页异常。

第一次请求页

失败,由于第一次访问虚拟地址address,内核还没建立address的页表和页表项,请求页失败,会进行缺页处理,建立虚拟地址address的页表和页表项映射。
具体流程:

1
2
3
4
5
6
__get_user_pages():首先进行进程调度,放弃当前CPU
--> follow_page():调用该函数请求物理页,<-- first loop。第一次虚拟地址adress,页表和页表项都为空,说明还没有加载过该页,进入循环,进程缺页处理
--> handle_mm_fault():为虚拟地址建立页表然后调用handle_pte_fault为虚拟地址建立页表项与物理页的映射
-->handle_pte_fault():由于对应物理页没有在内存,并且pte为空,说明之前没有加载过该页。又因为这是一个基于文件的映射,所以会调用do_linear_fault函数进行处理
-->do_linear_fault():处理线性文件映射的缺页
--> __do_fault():由于是写访问,并且对应的vma是MAP_PRIVATE,会触发cow操作,即将分配的新页与pte建立映射。也就是说当前的pte执行的是cowed page。

此时cow page属于匿名页,并且present = 1, dirty = 1, read-only = 1。还是不可写。

第二次请求页

第一次请求物理页,产生缺页异常,进行缺页处理,此时已经建立了address的页表和页表项与物理页的映射,这里是pte与cowed page的映射。
具体流程:

1
2
3
4
--> follow_page():调用该函数请求物理页,<-- second loop。这一次虚拟地址adress,页表和页表项都不为。但是由于是访问写,但是对于的vma属性为read-only,请求页失败,进入循环进行缺页处理。
--> handle_mm_fault()
-->handle_pte_fault():此时对应物理页在内存,判断异常是由写访问引起,并且对应物理页为read-only,进程cow操作
-->do_wp_page():处理那些用户试图修改pte页表没有可写属性的页面,它新分配一个页面并且复制旧页面内容到新的页面中。

do_wp_page这里会先获取 old page ,判断是否是匿名页,如果是匿名页,判断是否只有一个进程在引用该页或,如果是,则直接reuse old page,并尝试设置old page 标记为可写,并返回VM_FAULT_WRITE。否则需要新分配一个页作为cowed page,并返回VM_FAULT_WRITE。在循环中,如果检测到返回值为VM_FAULT_WRITE,为去掉请求访问FOLL_WRITE标志。

第三次请求页

由于第二次触发了COW操作,会返回VM_FAULT_WRITE,去掉了FOLL_WRITE标记,所以再次调用follow_page()函数请求页,会成功返回cowed page。因为此时pte对于的是cowed page,而不是原始页。

存在条件竞争过程分析

由于madvise系统调用可以使用指定内存,所以在正常请求物理页的第二步完成后,由于条件竞争,释放cowed page,释放操作会清空pte。此时进程切换回来,继续请求页,发现pte为空,触发缺页异常,但是此时访问请求标记没有了FOLL_WRITE,所以会认为是一个读访问,不会触发COW,这次缺页处理会填充pte对应原始物理页,再次调用follow_page成功获取原始页,所以正常情况会在cowed page上进行读写操作,但是由于条件经常的存在,实际会在原始的物理页上进行读写,也就造成只读文件可写。

总结

读了几天源码,总算是把这个漏洞成因搞清楚了,期间好几次把自己绕进去,哎,被自己蠢哭。不过分析过程还挺有意思。

0xFF Reference

https://www.anquanke.com/post/id/89096#h2-4
https://www.cnblogs.com/huxiao-tee/p/4660352.html
https://blog.csdn.net/vanbreaker/article/details/7881206