Technical Breakdown & Root Cause
1. Involved Files, Functions, Structs, and Fields
- Source Files:
- Structs & Fields:
struct vm_fault: vmf->address, vmf->pmd, vmf->pte, vmf->ptl, vmf->pgoff, vmf->page, vmf->vma
struct vm_area_struct: vma->vm_start, vma->vm_end, vma->vm_pgoff, vma->vm_flags
struct folio: folio_nr_pages(folio) (nr_pages), folio_page_idx(folio, page) (idx)
2. Invariant and Lifecycle Rule Violated
In the Linux page fault handling architecture, a page table page (allocated for a specific PMD entry via pte_alloc()) consists of exactly one 4KB physical page containing PTRS_PER_PTE entries (512 entries of 8 bytes on x86-64), covering a contiguous virtual address range of size PMD_SIZE (2MB).
As stated in the documentation and context requirements of set_ptes() (include/linux/pgtable.h):
Context: The caller holds the page table lock. The pages all belong to the same folio. The PTEs are all in the same PMD.
A single call to pte_offset_map_lock(mm, vmf->pmd, addr, &vmf->ptl) returns a pointer pte_t * into the single page table page associated with vmf->pmd. Any subsequent operation that accesses or updates nr_pages consecutive PTEs starting from this pointer (pte_range_none() or set_pte_range()) MUST NOT exceed the end of that page table page:
$$\text{pte_index}(addr) + nr_pages \le \text{PTRS_PER_PTE}$$
If this invariant is violated, pointer arithmetic on pte + i advances past the 4096-byte boundary of the page table page into the next contiguous physical page in the direct physical mapping.
3. How the Bug is Triggered and Mechanics of the Crash
-
Large Folio Creation in Anonymous Shmem:
A process creates a shared anonymous mapping via mmap(..., MAP_SHARED | MAP_ANONYMOUS), which is backed by tmpfs/shmem (vma_is_anon_shmem(vma) == true). It invokes madvise(..., MADV_COLLAPSE), producing a PMD-sized large folio (HPAGE_PMD_ORDER, 512 pages / 2MB) in the shmem file at pgoff = 0.
-
Unaligned Mapping:
The process maps file offset 0 (pgoff = 0) to an address that is not PMD-alignedβfor instance, via remap_file_pages() mapping to address 0x2000002ec000 with pgoff = 0. Here, the starting virtual address within the PMD has a page offset of 0xec (PTE index 236).
-
PMD Fault Fallback:
When a fault occurs on the VMA, finish_fault() is invoked. It first attempts to map the PMD as a huge page via do_set_pmd(). However, do_set_pmd() checks thp_vma_suitable_order(), which fails because the virtual start address of the VMA is not PMD-aligned ((vma->vm_start >> PAGE_SHIFT) - vma->vm_pgoff is not aligned to HPAGE_PMD_NR). do_set_pmd() returns VM_FAULT_FALLBACK.
-
Faulting Large Folio across PTEs without PMD Boundary Checks:
After do_set_pmd() falls back, finish_fault() allocates a standard PTE page for vmf->pmd and inspects the folio:
folio = page_folio(page);
nr_pages = folio_nr_pages(folio); // nr_pages = 512
...
} else if (nr_pages > 1) {
pgoff_t idx = folio_page_idx(folio, page); // idx = 0
pgoff_t vma_off = vmf->pgoff - vmf->vma->vm_pgoff;
if (unlikely(vma_off < idx ||
vma_off + (nr_pages - idx) > vma_pages(vma))) {
nr_pages = 1;
} else {
addr = vmf->address - idx * PAGE_SIZE; // addr = 0x2000002ec000
page = &folio->page;
}
}
finish_fault() checks only whether [addr, addr + nr_pages * PAGE_SIZE) stays within the VMA's boundaries (vma_pages(vma)). It does not check whether the range stays within the single PMD covered by vmf->pmd, nor does it verify that pte_index(addr) + nr_pages <= PTRS_PER_PTE.
-
Out-of-Bounds Access in pte_range_none() and set_pte_range():
finish_fault() locks the page table:
vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd, addr, &vmf->ptl);
Because addr has page offset 0xec = 236, vmf->pte points to entry 236 of the page table page (pfn: 0x7bfb6, virtual 0xffff88807bfb6000). Only $512 - 236 = 276$ PTE entries remain in this 4KB page.
Next, finish_fault() checks:
else if (nr_pages > 1 && !pte_range_none(vmf->pte, nr_pages))
pte_range_none() loops i from 0 to 511 (nr_pages):
for (i = 0; i < nr_pages; i++) {
if (!pte_none(ptep_get_lockless(pte + i)))
return false;
}
When i reaches 276, pte + 276 accesses offset 236 * 8 + 276 * 8 = 4096 bytes from the base of the page table page, crossing into the base of the next physical page (pfn: 0x7bfb7, virtual address 0xffff88807bfb7000). Because that adjacent page happens to be free, KASAN immediately detects a use-after-free read of size 8 at 0xffff88807bfb7000.