fix(paging): validate non-leaf page-table entries before dereference

_set_page_writable / MakePTWritable derived table pointers with
entry & ~0xFFF, which keeps reserved bits 52..62 and NX. An invalid
entry (P=1, PS=0, reserved bits set) produced 0xfffffffffffff000 and
a boot-time #PF in _set_page_writable.

Add _table_phys: keep address bits 12..51 and require reserved bits
52..62 to be zero. Use it at every non-leaf level in both walkers.
This commit is contained in:
TermiNexus committed 2026-10-27 16:35:06 +08:00
1 parent da4b0e47b7
commit f9cf26e59a
1 file changed
+38 -32
+38 -32
View File
@@ -274,73 +274,79 @@ def EnablePaging():
global pml4, pdp, pd, pt
__asm__(f"mov cr3, {asminp(uint64(pml4), ASM_DESCR.REG_ANY)}")
def _set_page_writable(phys_addr: uint64):
"""把物理页 phys_addr(identity 映射,虚拟=物理)的 PTE 置 W 位
# Extract the physical address of a non-leaf page-table entry.
# Bits 12..51 hold the address; bits 52..62 are reserved and must be zero.
# Bit 63 (NX) is ignored for non-leaf entries. Returns 0 for unusable entries.
def _table_phys(entry: uint64) -> uint64:
if (entry & PTE_PRESENT) == 0: return 0
if (entry & uint64(0x7FF0000000000000)) != 0: return 0
return entry & uint64(0x000FFFFFFFFFF000)
页表页(bootloader 建立的 pml4/pdp/pd/pt 位于 0xf8xxxx)默认只读,
而内核运行时 MapPage/clone 需修改页表 → 必须先把这些页表页自身设为可写,
否则 CR0.WP=1 下写页表触发 #PF(slog.so 映射 0x600000 新建页表项时
cr2=0xf801000 err=0x3 根因)。
def _set_page_writable(phys_addr: uint64):
"""Mark the identity-mapped page containing phys_addr writable via its PTE.
Page-table pages created by the bootloader are read-only; MapPage and
clone need to modify them, so they must be made writable while CR0.WP
is clear. Non-leaf entries are validated with _table_phys before being
dereferenced.
"""
global pml4
if not pml4: return
va: uint64 = phys_addr & ~uint64(0xFFF)
if va == 0: return
pml4_idx: uint64 = (va >> 39) & 0x1FF
if not (pml4.entries[pml4_idx] & PTE_PRESENT): return
pdp_s: _p* = _p*(pml4.entries[pml4_idx] & ~uint64(0xFFF))
pdp_phys: uint64 = _table_phys(pml4.entries[pml4_idx])
if pdp_phys == 0: return
pdp_s: _p* = _p*(pdp_phys)
pdp_idx: uint64 = (va >> 30) & 0x1FF
if not (pdp_s.entries[pdp_idx] & PTE_PRESENT): return
if pdp_s.entries[pdp_idx] & PTE_PS:
# 1GB 大页(PDPTE.PS=1,固件 identity 映射常见):没有下层 PD,W 位在本条目自身。
# 若仍按 `& ~0xFFF` 取"PD 基址",条目位 63(NX,固件对 1GB 数据页常置 1)会一起被带出来
# → 派生指针非规范 → 读 %rax+idx*8 即 #GP(0)(现场 rip=0x11a93e,偶发取决于该 VA 是否
# 落进 1GB 大页)。与下面 2MB 分支同口径:直接置本条目 W 位。
# 1GB page: the W bit lives in this entry; there is no lower PD.
pdp_s.entries[pdp_idx] = pdp_s.entries[pdp_idx] | uint64(PTE_WRITABLE)
__asm__(f"invlpg [{asminp(va, ASM_DESCR.REG_ANY)}]")
return
pd_s: _p* = _p*(pdp_s.entries[pdp_idx] & ~uint64(0xFFF))
pd_phys: uint64 = _table_phys(pdp_s.entries[pdp_idx])
if pd_phys == 0: return
pd_s: _p* = _p*(pd_phys)
pd_idx: uint64 = (va >> 21) & 0x1FF
if not (pd_s.entries[pd_idx] & PTE_PRESENT): return
if pd_s.entries[pd_idx] & PTE_PS:
# 2MB 大页:W 位在 PD 条目自身(无下层 PT),直接置位
# 2MB page: the W bit lives in this entry; there is no lower PT.
pd_s.entries[pd_idx] = pd_s.entries[pd_idx] | uint64(PTE_WRITABLE)
__asm__(f"invlpg [{asminp(va, ASM_DESCR.REG_ANY)}]")
return
pt_s: _p* = _p*(pd_s.entries[pd_idx] & ~uint64(0xFFF))
pt_phys: uint64 = _table_phys(pd_s.entries[pd_idx])
if pt_phys == 0: return
pt_s: _p* = _p*(pt_phys)
pt_idx: uint64 = (va >> 12) & 0x1FF
pt_s.entries[pt_idx] = pt_s.entries[pt_idx] | uint64(PTE_WRITABLE)
__asm__(f"invlpg [{asminp(va, ASM_DESCR.REG_ANY)}]")
def MakePTWritable():
"""把当前页表层次所有页表页(pml4 自身 + pdp/pd/pt)设为可写
"""Make all page-table pages currently reachable from pml4 writable.
内核运行时需修改页表(MapPage/clone_for_process),而 bootloader 建立的
页表页映射为只读——CR0.WP=1 下必须先放开页表页自身,否则写页表即 #PF。
Non-leaf entries are validated with _table_phys. Huge pages (PS=1) are
leaves, so they are not walked; _set_page_writable updates their own W bit.
"""
global pml4
if not pml4: return
_set_page_writable(uint64(pml4))
for pi in range(512):
if not (pml4.entries[pi] & PTE_PRESENT): continue
pdp_m: _p* = _p*(pml4.entries[pi] & ~uint64(0xFFF))
pdp_phys: uint64 = _table_phys(pml4.entries[pi])
if pdp_phys == 0: continue
pdp_m: _p* = _p*(pdp_phys)
_set_page_writable(uint64(pdp_m))
pj: uint64
for pj in range(512):
if not (pdp_m.entries[pj] & PTE_PRESENT): continue
# 1GB 大页(PDPTE.PS=1)不是页表页:把它当 PD 解引用,会把大页内存内容当表项读
# (其中约一半 qword 的 bit0=1)→ 按"表项"递归 _set_page_writable 随机地址 → 任意
# 内存被置 W 位(偶发数据损坏)且可算出非规范指针 → #GP。跳过(与下面 2MB 的
# `if PTE_PS: continue` 同口径);若页表页本身落在 1GB 大页里,则由上面
# _set_page_writable(该页地址) 命中 1GB 分支把大页条目的 W 位置起。
if pdp_m.entries[pj] & PTE_PS: continue
pd_m: _p* = _p*(pdp_m.entries[pj] & ~uint64(0xFFF))
pd_phys: uint64 = _table_phys(pdp_m.entries[pj])
if pd_phys == 0: continue
pd_m: _p* = _p*(pd_phys)
_set_page_writable(uint64(pd_m))
pk: uint64
for pk in range(512):
if not (pd_m.entries[pk] & PTE_PRESENT): continue
if pd_m.entries[pk] & PTE_PS: continue
pt_m: _p* = _p*(pd_m.entries[pk] & ~uint64(0xFFF))
_set_page_writable(uint64(pt_m))
pt_phys: uint64 = _table_phys(pd_m.entries[pk])
if pt_phys == 0: continue
_set_page_writable(pt_phys)
def MakeRangeRO(vaddr_start: uint64, vaddr_end: uint64):
"""把 [vaddr_start, vaddr_end) 范围内所有页的 PTE 清 W 位(只读)