Linux X86针对meltdown漏洞开启了页表隔离(PTI)功能,PTI使用两组PGD来表示整个进程空间.在进程陷入内核态时,CR3会相应的切换到进程内核态的PGD页表.
假如进程调用了vmalloc分配内存,此时会进行页表映射,但是vmalloc是把这块内存映射到了内核的虚拟地址空间(init_mm),并没有映射到当前进程的内核态页表.
vmalloc->__vmalloc_node_range->__vmalloc_area_node->map_vm_area->vmap_page_range->vmap_page_range_noflush->pgd_offset_k //建立内核空间页表
#define pgd_offset_k(address) pgd_offset(&init_mm, (address))
所以当ring3进程陷入内核态,并访问vmalloc分配的内存时,这里会产生内核态缺页异常.
处理流程:
do_page_fault->__do_page_fault->do_kern_addr_fault->vmalloc_fault
在vmalloc_fault中,会复制init_mm的页表到当前CR3指向的页表。
static noinline int vmalloc_fault(unsigned long address)
{
pgd_t *pgd, *pgd_k;
p4d_t *p4d, *p4d_k;
pud_t *pud;
pmd_t *pmd;
pte_t *pte;
/* Make sure we are in vmalloc area: */
if (!(address >= VMALLOC_START && address < VMALLOC_END))
return -1;
WARN_ON_ONCE(in_nmi());
/*
* Copy kernel mappings over when needed. This can also
* happen within a race in page table update. In the later
* case just flush:
*/
/*读取当前进程内核态PGD */
pgd = (pgd_t *)__va(read_cr3_pa()) + pgd_index(address);
/*pgd_k为vmalloc建立的内核页表 */
pgd_k = pgd_offset_k(address);
if (pgd_none(*pgd_k))
return -1;
/*下面就是依次复制内核页表的P4D->PUD->PMD->PTE */
/* With 4-level paging, copying happens on the p4d level. */
p4d = p4d_offset(pgd, address);
p4d_k = p4d_offset(pgd_k, address);
if (p4d_none(*p4d_k))
return -1;
if (p4d_none(*p4d) && !pgtable_l5_enabled()) {
set_p4d(p4d, *p4d_k);
arch_flush_lazy_mmu_mode();
} else {
BUG_ON(p4d_pfn(*p4d) != p4d_pfn(*p4d_k));
}
pud = pud_offset(p4d, address);
if (pud_none(*pud))
return -1;
pmd = pmd_offset(pud, address);
if (pmd_none(*pmd))
return -1;
pte = pte_offset_kernel(pmd, address);
if (!pte_present(*pte))
return -1;
return 0;
}