Files
TermiNexus 9745f8832a fix(vkernel): UHCI frame list 步长越界 12KB 砸坏 buddy 堆(堆损坏根因);paging 页表记录指针缩放;清理临时探针
根因:uhci.vp 两处写成 _frame_list + uint64(i) * 4 —— uint32* 的 +n 已按元素(4字节)缩放,
再乘 4 使实际步长变成 16 字节;1024 次循环写到 16KB,而缓冲只有 UHCI_NUM_FRAMES*4 = 4KB
⇒ 越界 12KB 覆盖相邻 buddy 空闲块头(现场:[0E][PF] cr2=0xba4c000 err=0x3 rip=0x10e598
rax=0x3000=12288),把空闲链 next/prev 写坏 ⇒ 随机 [mm] BAD blk、malloc 失败、app 崩溃。
定位手段:把空闲链表头页在内核页表标只读(paging.init 已置 CR0.WP=1),任何写入立即 #PF
并给出 rip/cr2 直接指认写者(该探针定位完已删除)。

paging:save_pt_range / restore_pt_range 里的 save_buf + i*16 在 uint64* 上实际是 128 字节
步长、+8 实际是 +64 字节(IR 取证:getelementptr i64 ⇒ 按 8 字节元素缩放),改为显式字节
地址推进记录 + 元素索引(+1 = +8 字节),两处布局保持一致。

保留的加固(非探针):memory_block.magic 与 allocate_block/free_block/merge_blocks 的块头
校验、free() 的堆范围校验、slab 建页的 total 边界护栏、页表池按真实 buf_size 计页与
pt_pool_magic/pt_pool_limit 哨兵、#GP 分支补打 rax 便于定位非规范地址写。

elf.vp 新增 ELF_LOAD 开关(=1 正常装载;置 0 可整体跳过 app/.so 装载,便于隔离内核问题)。
2026-10-15 13:35:34 +08:00

591 lines
29 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import drivers.serial.uart.serial as serial
import viperlib
define PTE_PRESENT = (1 << 0)
define PTE_WRITABLE = (1 << 1)
define PTE_USER = (1 << 2)
define PTE_PWT = (1 << 3)
define PTE_PCD = (1 << 4)
define PTE_ACCESSED = (1 << 5)
define PTE_DIRTY = (1 << 6)
define PTE_PS = (1 << 7)
define PTE_GLOBAL = (1 << 8)
class _p:
entries: [512]uint64
define PT_POOL_PAGES = 512
pt_pool_raw: rawptr = None
pt_pool_start: uint64 = 0
pt_pool_used: uint64 = 0
# 实际可用页数(由 init_pool 依调用者给的 buf_size 算出,上限 PT_POOL_PAGES)。
# 不能用硬编码 PT_POOL_PAGES 当唯一上限:池的缓冲由调用者提供,缓冲小于
# PT_POOL_PAGES*4K 时,池会发到缓冲之外(那些页属于 buddy 空闲块/别人内存)。
pt_pool_pages: uint64 = 0
# 池全局量的完整性哨兵:pt_pool_start/pt_pool_pages 一旦被越界写覆盖,池就会把页表页发到
# 任意地址(例如 buddy 的空闲块 0xba4c000 上)—— 那正是本文件 line ~444 注释里的老怀疑。
define PT_POOL_MAGIC = 0x50544C21 # "PTL!"
pt_pool_magic: uint64 = 0
pt_pool_limit: uint64 = 0 # pt_pool_start + pt_pool_pages*4096
AllocatedPages: uint64 = 0
pml4: _p* = None
pdp: _p*
pd: _p*
pt: _p*
def init(MemmapAddr: uint64, MemmapSize: uint64):
global pml4
pml4 = GetCurrentPml4()
# bootloader/UEFI 的 CR0.WP 通常已是 1(原代码清 WP 正因此)。页表页
# (pml4/pdp/pd/pt,0xf8xxxx)默认只读,而内核运行时 MapPage/clone 需
# 修改页表。顺序:先清 WP(允许写只读页表页)→ MakePTWritable 放开
# 全部页表页 → 再置 WP=1(此后写只读页即 #PF 暴露越界写源)。
__asm__(f"""
mov rax, cr0
and rax, ~0x10000
mov cr0, rax
""")
MakePTWritable()
__asm__(f"""
mov rax, cr0
or rax, 0x10000
mov cr0, rax
""")
def init_pool(buf: rawptr, buf_size: uint64):
global pt_pool_raw, pt_pool_start, pt_pool_used, pt_pool_pages, pt_pool_magic, pt_pool_limit
pt_pool_raw = buf
if buf is None:
pt_pool_pages = 0
pt_pool_magic = 0
pt_pool_limit = 0
return
raw_addr: uint64 = uint64(buf)
aligned_addr: uint64 = (raw_addr + uint64(0xFFF)) & ~uint64(0xFFF)
pt_pool_start = aligned_addr
pt_pool_used = 0
# 边界以调用者给的 buf_size 为准。此前只保存 buf、把 buf_size 丢掉,AllocPage 又拿硬编码
# PT_POOL_PAGES 当唯一上限 ⇒ 缓冲小于 PT_POOL_PAGES*4K 时池会发到缓冲之外,那几页属于
# buddy 空闲块/别人的内存,页表内容会直接覆盖它们(现场:buddy 块头 size/next 被
# "页基址|0x22" 形态的 PTE 成片覆盖 ⇒ 空闲链坏 ⇒ 后续 allocate_block -> NULL)。
slack: uint64 = aligned_addr - raw_addr
avail: uint64 = 0
if buf_size > slack:
avail = (buf_size - slack) / uint64(4096)
if avail > uint64(PT_POOL_PAGES): avail = uint64(PT_POOL_PAGES)
pt_pool_pages = avail
pt_pool_limit = pt_pool_start + avail * uint64(4096)
pt_pool_magic = uint64(PT_POOL_MAGIC)
if avail < uint64(PT_POOL_PAGES):
serial.puts("[ptpool] ERROR: caller buffer smaller than PT_POOL_PAGES - capped, later exhaustion expected\n")
def AllocPage() -> rawptr:
global AllocatedPages, pt_pool_used, pml4, pdp, pd, pt
if pt_pool_raw is None: return None
# 哨兵:池的全局量被越界写覆盖时,池会把页表页发到任意地址(例如 buddy 空闲块 0xba4c000)
# ⇒ 页表内容直接覆盖别人的块头。这里先自证清白,再发页。
if pt_pool_magic != uint64(PT_POOL_MAGIC):
_pmb: [192]char
viperlib.snprintf(&_pmb, 192, "[ptpool] CORRUPT: magic=0x%lx start=0x%lx used=%lu pages=%lu - refusing to hand out pages\n",
pt_pool_magic, pt_pool_start, pt_pool_used, pt_pool_pages)
serial.puts(&_pmb)
return None
# 上限用"真实可用页数"(init_pool 依 buf_size 算出);耗尽时显式报出,
# 既不静默失败、也绝不越界发页(越界发的页会覆盖别人内存)。
if pt_pool_used >= pt_pool_pages:
_apb: [176]char
viperlib.snprintf(&_apb, 176, "[ptpool] EXHAUSTED: used=%lu/%lu pages - raise PT_POOL_PAGES or the caller buffer\n",
pt_pool_used, pt_pool_pages)
serial.puts(&_apb)
return None
page: rawptr = rawptr(pt_pool_start + pt_pool_used * uint64(4096))
pt_pool_used = pt_pool_used + uint64(1)
# rep stosb 向 [rdi] 写 4096 字节:必须声明 CLOBBER_MEMORY,否则优化器
# 无法得知本块改写了内存,可能把其前后的内存读写重排/消除(N8)。
__asm__(f"""mov al, 0
mov ecx, 4096
mov rdi, {asminp(page, ASM_DESCR.REG_ANY)}
rep stosb""",
op = [ASM_DESCR.CLOBBER_MEMORY, ASM_DESCR.CLOBBER_AL,
ASM_DESCR.CLOBBER_ECX, ASM_DESCR.CLOBBER_RDI])
AllocatedPages = AllocatedPages + uint64(1)
return page
def FreePage(page: rawptr):
global AllocatedPages, pml4, pdp, pd, pt
if page is None: return
if AllocatedPages > 0: AllocatedPages = AllocatedPages - uint64(1)
# ============================================================
# _split_1gb_to_pd - 1GB 大页(PDPTE.PS=1)拆成 512 个 2MB 的 PD
# ============================================================
# 固件的 identity 映射常以 1GB 大页表示一大片(不可执行)内存。此时 PDPTE 没有下层 PD,
# 若把它当 PD 用(`pd.entries[i] = ...`),页表项会写进**大页内存本身**(写坏数据),
# 读到的“表项”是大页内存内容(随机,约一半 qword bit0=1)→ 据此再派生指针即非规范地址
# → #GP(0)(现场 rip=0x11a93e)。拆分后 PDPTE 指向新建 PD 且 PS 清 0,每个 2MB 条目
# 继承原 1GB 条目的 PCD/PWT/USER/NX,后续沿用既有 2MB/4KB 逻辑。
# 参数用 uint64 物理地址(与本文件 MapPage/UnMapPage/_set_page_writable 同口径,
# 不引入结构体指针形参),函数内自行重建 _p*。
# 返回 1 = 可继续(条目本来就不存在、或本来就是 PD、或拆分成功);0 = 拆分失败(页表池耗尽)。
# 注意:条目**不存在**时返回 1 —— 此时“建 PD”由调用方既有逻辑处理(MapPage/MapLargePage
# 的 `if not present: pd = AllocPage()`),本函数不能替它失败。
def _split_1gb_to_pd(pdp_pa: uint64, pdp_idx: uint64) -> int:
if pdp_pa == 0: return 0
pdp_t: _p* = _p*(pdp_pa & ~uint64(0xFFF))
e: uint64 = pdp_t.entries[pdp_idx]
if not (e & PTE_PRESENT): return 1
if not (e & PTE_PS): return 1
base: uint64 = e & ~uint64(0x3FFFFFFF)
inherit: uint64 = e & (PTE_PCD | PTE_PWT | PTE_USER | uint64(0x8000000000000000))
new_pd: _p* = AllocPage()
if not new_pd: return 0
for si in range(512):
new_pd.entries[si] = (base + uint64(si) * uint64(0x200000)) | PTE_PRESENT | PTE_WRITABLE | PTE_PS | inherit
# 新 PDPTE 是**非叶条目**:只带 PRESENT|WRITABLE|USER,**绝不带 bit63(NX)** ——
# NX 生效位置是叶条目,已由上面 512 个 2MB 条目继承;非叶条目带 bit63 会让后续所有
# `entry & ~0xFFF` 派生出的表指针变成非规范地址(正是刚修的 #GP 根因)。
pdp_t.entries[pdp_idx] = (uint64(new_pd) & ~uint64(0xFFF)) | PTE_PRESENT | PTE_WRITABLE | (inherit & PTE_USER)
return 1
def MapPage(VirtAddr: uint64, PhysAddr: uint64, flags: uint64):
global pml4, pdp, pd, pt
if VirtAddr == 0 or PhysAddr == 0: return
VirtAddr = VirtAddr & ~uint64(0xFFF)
PhysAddr = PhysAddr & ~uint64(0xFFF)
Pml4Index: uint64 = (VirtAddr >> 39) & 0x1FF
PdpIndex: uint64 = (VirtAddr >> 30) & 0x1FF
PdIndex: uint64 = (VirtAddr >> 21) & 0x1FF
PtIndex: uint64 = (VirtAddr >> 12) & 0x1FF
if not pml4:
pml4 = AllocPage()
if not pml4: return
if not (pml4.entries[Pml4Index] & PTE_PRESENT):
pdp = AllocPage()
if not pdp: return
pml4.entries[Pml4Index] = (uint64(pdp) & ~uint64(0xFFF)) | PTE_PRESENT | PTE_WRITABLE | (flags & PTE_USER)
else:
if (flags & PTE_USER) and not (pml4.entries[Pml4Index] & PTE_USER):
pml4.entries[Pml4Index] = pml4.entries[Pml4Index] | PTE_USER
pdp = _p*(pml4.entries[Pml4Index] & ~uint64(0xFFF))
# 1GB 大页(PDPTE.PS=1):先拆成 512 个 2MB 的 PD,否则下面的 pd.entries[...] 会写进
# 大页内存(写坏数据)并读到随机“表项”。拆完就是 2MB 条目,按既有逻辑继续。
if _split_1gb_to_pd(uint64(pdp), PdpIndex) == 0: return
if not (pdp.entries[PdpIndex] & PTE_PRESENT):
pd = AllocPage()
if not pd: return
pdp.entries[PdpIndex] = (uint64(pd) & ~uint64(0xFFF)) | PTE_PRESENT | PTE_WRITABLE | (flags & PTE_USER)
else:
if (flags & PTE_USER) and not (pdp.entries[PdpIndex] & PTE_USER):
pdp.entries[PdpIndex] = pdp.entries[PdpIndex] | PTE_USER
pd = _p*(pdp.entries[PdpIndex] & ~uint64(0xFFF))
if pd.entries[PdIndex] & 1:
if pd.entries[PdIndex] & 0x80:
big_phys: uint64 = pd.entries[PdIndex] & ~uint64(0x1FFFFF)
inherit_flags: uint64 = pd.entries[PdIndex] & (PTE_PCD | PTE_PWT)
pt = AllocPage()
if not pt: return
for split_i in range(512):
pt.entries[split_i] = (big_phys + uint64(split_i) * 0x1000) | PTE_PRESENT | PTE_WRITABLE | inherit_flags
pd.entries[PdIndex] = (uint64(pt) & ~uint64(0xFFF)) | PTE_PRESENT | PTE_WRITABLE | (flags & PTE_USER)
else:
# PD entry exists as a regular PT (e.g. cloned identity mapping).
# Must propagate PTE_USER to the PD entry so user-mode access is
# allowed at all paging levels, otherwise #PF err=0x5.
if (flags & PTE_USER) and not (pd.entries[PdIndex] & PTE_USER):
pd.entries[PdIndex] = pd.entries[PdIndex] | PTE_USER
else:
pt = AllocPage()
if not pt: return
pd.entries[PdIndex] = (uint64(pt) & ~uint64(0xFFF)) | PTE_PRESENT | PTE_WRITABLE | (flags & PTE_USER)
pt = _p*(pd.entries[PdIndex] & ~uint64(0xFFF))
pt.entries[PtIndex] = PhysAddr | flags | PTE_PRESENT
__asm__(f"invlpg [{asminp(VirtAddr, ASM_DESCR.REG_ANY)}]")
def MapLargePage(VirtAddr: uint64, PhysAddr: uint64, flags: uint64):
global pml4, pdp, pd, pt
VirtAddr = VirtAddr & ~uint64(0x1FFFFF)
PhysAddr = PhysAddr & ~uint64(0x1FFFFF)
Pml4Index: uint64 = (VirtAddr >> 39) & 0x1FF
PdpIndex: uint64 = (VirtAddr >> 30) & 0x1FF
PdIndex: uint64 = (VirtAddr >> 21) & 0x1FF
if not pml4:
pml4 = AllocPage()
if not pml4: return
if not pml4.entries[Pml4Index] & PTE_PRESENT:
pdp = AllocPage()
if not pdp: return
pml4.entries[Pml4Index] = (uint64(pdp) & ~uint64(0xFFF)) | PTE_PRESENT | PTE_WRITABLE
pdp = _p*(pml4.entries[Pml4Index] & ~uint64(0xFFF))
# 1GB 大页(PDPTE.PS=1):先拆成 PD,否则下面会把 2MB 条目写进大页内存(写坏数据)。
if _split_1gb_to_pd(uint64(pdp), PdpIndex) == 0: return
if not (pdp.entries[PdpIndex] & PTE_PRESENT):
pd = AllocPage()
if not pd: return
pdp.entries[PdpIndex] = (uint64(pd) & ~uint64(0xFFF)) | PTE_PRESENT | PTE_WRITABLE
pd = _p*(pdp.entries[PdpIndex] & ~uint64(0xFFF))
pd.entries[PdIndex] = PhysAddr | flags | PTE_PRESENT | PTE_PS
__asm__(f"invlpg [{asminp(VirtAddr, ASM_DESCR.REG_ANY)}]")
def UnMapPage(VirtAddr: uint64):
global pml4, pdp, pd, pt
if VirtAddr == 0: return
if not pml4: return
Pml4Index: uint64 = (VirtAddr >> 39) & 0x1FF
PdpIndex: uint64 = (VirtAddr >> 30) & 0x1FF
PdIndex: uint64 = (VirtAddr >> 21) & 0x1FF
PtIndex: uint64 = (VirtAddr >> 12) & 0x1FF
if not pml4.entries[Pml4Index] & PTE_PRESENT: return
pdp = _p*(pml4.entries[Pml4Index] & ~uint64(0xFFF))
if not pdp.entries[PdpIndex] & PTE_PRESENT: return
# 1GB 大页(PDPTE.PS=1):先拆成 2MB 的 PD,否则下面会把大页内存内容当表项 →
# 往派生出的随机地址写 0(此前“fb/pitch/w/h 被清零”那类隐性写坏)。拆完按既有
# 口径撤除命中的条目(PS 覆盖时整条撤除,与 2MB 分支一致)。
if _split_1gb_to_pd(uint64(pdp), PdpIndex) == 0: return
pd = _p*(pdp.entries[PdpIndex] & ~uint64(0xFFF))
if not pd.entries[PdIndex] & PTE_PRESENT: return
if pd.entries[PdIndex] & PTE_PS:
pd.entries[PdIndex] = 0
else:
pt = _p*(pd.entries[PdIndex] & ~uint64(0xFFF))
pt.entries[PtIndex] = 0
__asm__(f"invlpg [{asminp(VirtAddr, ASM_DESCR.REG_ANY)}]")
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 位
页表页(bootloader 建立的 pml4/pdp/pd/pt 位于 0xf8xxxx)默认只读,
而内核运行时 MapPage/clone 需修改页表 → 必须先把这些页表页自身设为可写,
否则 CR0.WP=1 下写页表触发 #PF(slog.so 映射 0x600000 新建页表项时
cr2=0xf801000 err=0x3 根因)。
"""
global pml4
if not pml4: return
va: uint64 = phys_addr & ~uint64(0xFFF)
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_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 位。
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_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),直接置位
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_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)设为可写
内核运行时需修改页表(MapPage/clone_for_process),而 bootloader 建立的
页表页映射为只读——CR0.WP=1 下必须先放开页表页自身,否则写页表即 #PF。
"""
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))
_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))
_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))
def MakeRangeRO(vaddr_start: uint64, vaddr_end: uint64):
"""把 [vaddr_start, vaddr_end) 范围内所有页的 PTE 清 W 位(只读)
定位越界写源:内核态写只读页在 CR0.WP=1 下触发 #PF(int=14),
isrCommonhandler 打印 cr2/rip → 写源立即暴露,不再静默破坏 .rodata。
"""
global pml4
if not pml4: return
va: uint64 = vaddr_start & ~uint64(0xFFF)
# ro_end 下取整到页:不保护含 ro_end 的未对齐尾页。链接脚本输出段未页对齐,
# .data 紧跟 .rodata(__rodata_end 未对齐时 .data 起始落在 rodata 尾页内),
# 整页置只读会把 .data 全局(如 devfs @DevFSObject=0x141630)误保护 →
# devfs_init 写全局 #PF(cr2=0x141630 rip=0x101aaa mov [r15],rax 根因)。
# 尾页含少量 rodata 尾部(对齐填充),保持可写可容忍。
ro_end_align: uint64 = vaddr_end & ~uint64(0xFFF)
while va < ro_end_align:
pml4_idx: uint64 = (va >> 39) & 0x1FF
if not (pml4.entries[pml4_idx] & PTE_PRESENT): va += 0x1000; continue
pdp_r: _p* = _p*(pml4.entries[pml4_idx] & ~uint64(0xFFF))
pdp_idx: uint64 = (va >> 30) & 0x1FF
if not (pdp_r.entries[pdp_idx] & PTE_PRESENT): va += 0x1000; continue
# 1GB 大页(PDPTE.PS=1):同样不是 PD,当 PD 用会把大页内容当表项读(并据其"表项"
# 写页表 → 写坏任意内存)。1GB 大页拆分需新建 1 个 PD + 若干 PT(513 页),此处不支持
# → 跳过该页。后果:若 .rodata 落在 1GB 大页内则该段保持可写(不保护),但绝不再写坏内存。
if pdp_r.entries[pdp_idx] & PTE_PS: va += 0x1000; continue
pd_r: _p* = _p*(pdp_r.entries[pdp_idx] & ~uint64(0xFFF))
pd_idx: uint64 = (va >> 21) & 0x1FF
if not (pd_r.entries[pd_idx] & PTE_PRESENT): va += 0x1000; continue
if pd_r.entries[pd_idx] & PTE_PS:
# 2MB 大页:直接跳过会让 .rodata 保持可写——越界写被静默吞掉
# (perf 格式串 win=%lu/work=%lu 被逐步破坏的根因)。拆分为 512
# 个 4KB 页(复用 MapPage 的分裂逻辑),再走下方常规路径清 W 位。
# WP=1 后内核态写只读页即 #PF → isrCommonhandler 打印 cr2/rip 暴露写源。
big_phys: uint64 = pd_r.entries[pd_idx] & ~uint64(0x1FFFFF)
inherit_flags: uint64 = pd_r.entries[pd_idx] & (PTE_PCD | PTE_PWT | PTE_USER | uint64(0x8000000000000000))
pt_split: _p* = AllocPage()
if not pt_split:
va += 0x1000
continue
for split_i in range(512):
pt_split.entries[split_i] = (big_phys + uint64(split_i) * uint64(0x1000)) | PTE_PRESENT | PTE_WRITABLE | inherit_flags
pd_r.entries[pd_idx] = (uint64(pt_split) & ~uint64(0xFFF)) | PTE_PRESENT | PTE_WRITABLE | (pd_r.entries[pd_idx] & PTE_USER)
__asm__(f"invlpg [{asminp(va, ASM_DESCR.REG_ANY)}]")
# 分裂后落入下方 4KB 常规路径,继续清当前页 W 位
pt_r: _p* = _p*(pd_r.entries[pd_idx] & ~uint64(0xFFF))
pt_idx: uint64 = (va >> 12) & 0x1FF
pt_r.entries[pt_idx] = pt_r.entries[pt_idx] & ~uint64(PTE_WRITABLE)
__asm__(f"invlpg [{asminp(va, ASM_DESCR.REG_ANY)}]")
va += 0x1000
def ProtectKernelRO():
"""把内核镜像 .rodata 段(字符串常量/vtable 等)映射为只读
地址经 __asm__ 内联引用链接器符号 __rodata_start/__rodata_end
(linker.ld 定义,VPC 不声明 extern——避免生成定义与链接器符号冲突)。
"""
global pml4
if not pml4: return
# 页表页可写已在 paging.init(WP=0 阶段)由 MakePTWritable 完成,
# 此处只保护 .rodata 内容页(WP=1 后写只读页即 #PF 暴露写源)。
ro_start: uint64 = 0
ro_end: uint64 = 0
__asm__(f"""lea rax, [rip + __rodata_start]
mov {asmout(ro_start, ASM_DESCR.OUTPUT_REG)}, rax
lea rax, [rip + __rodata_end]
mov {asmout(ro_end, ASM_DESCR.OUTPUT_REG)}, rax""",
op=[ASM_DESCR.CLOBBER_RAX])
MakeRangeRO(ro_start, ro_end)
# [PG] 打印 .rodata 起始页映射:确认只读真正生效。
# ps=1 说明仍在 2MB 大页(分裂失败)→ .rodata 保持可写 → 越界写继续被静默吞掉。
DumpMapping(ro_start, "ro_s")
def DumpMapping(va: uint64, tag: const str):
"""[PG] 打印指定 VA 的 pml4/pdp/pd/pt 条目,定位大页(PS)与权限位
内核区域若为 UEFI 2MB 大页,MakeRangeRO 无法只读 .rodata(越界写静默破坏
格式串);1GB 页同理(PS 位在 pdpe)。W=1 且 PS=1 → 该页可写且是大页。
"""
global pml4
if not pml4: return
pml4_i: uint64 = (va >> 39) & 0x1FF
pdp_i: uint64 = (va >> 30) & 0x1FF
pd_i: uint64 = (va >> 21) & 0x1FF
pt_i: uint64 = (va >> 12) & 0x1FF
pml4_e: uint64 = pml4.entries[pml4_i]
pdp_e: uint64 = 0
pd_e: uint64 = 0
pt_e: uint64 = 0
if pml4_e & PTE_PRESENT:
pdp_r: _p* = _p*(pml4_e & ~uint64(0xFFF))
pdp_e = pdp_r.entries[pdp_i]
if pdp_e & PTE_PRESENT:
if pdp_e & PTE_PS:
pd_e = pdp_e
else:
pd_r: _p* = _p*(pdp_e & ~uint64(0xFFF))
pd_e = pd_r.entries[pd_i]
if (pd_e & PTE_PRESENT) and not (pd_e & PTE_PS):
pt_r: _p* = _p*(pd_e & ~uint64(0xFFF))
pt_e = pt_r.entries[pt_i]
buf_d: [200]char
viperlib.snprintf(&buf_d, 200, "[PG] %s va=0x%lx pdpe=0x%lx pde=0x%lx pte=0x%lx (ps=%d W=%d)\n",
tag, va, pdp_e, pd_e, pt_e,
1 if (pd_e & PTE_PS) else 0,
1 if (pt_e & PTE_WRITABLE) or (pd_e & PTE_WRITABLE) or (pdp_e & PTE_WRITABLE) else 0)
serial.puts(&buf_d)
def GetCurrentPml4() -> _p*:
global pml4, pdp, pd, pt
cr3: uint64
__asm__(f"mov {asmout(cr3, ASM_DESCR.OUTPUT_REG)}, cr3")
return _p*(cr3 & ~uint64(0xFFF))
def create_process_page_table() -> uint64:
global pml4
if not pml4: return 0
new_pml4: _p* = AllocPage()
if not new_pml4: return 0
i: uint64
for i in range(512):
entry: uint64 = pml4.entries[i]
if entry & PTE_PRESENT:
new_pml4.entries[i] = entry
return uint64(new_pml4)
def clone_for_process(isolate_vaddr: uint64) -> uint64:
global pml4
_dbg: [120]char
viperlib.snprintf(&_dbg, 120, "[clone] pml4=0x%lx pool_used=%lu iso_va=0x%lx\n", uint64(pml4), pt_pool_used, isolate_vaddr)
serial.puts(_dbg)
if not pml4: return 0
new_pml4_page: _p* = AllocPage()
if not new_pml4_page:
# [DIAG] 定位 AllocPage 失败:pool_used 打印 0 但分配失败 → BSS 全局
# (pt_pool_raw/pt_pool_used)疑似被内存越界写覆盖。
_dbgf: [120]char
viperlib.snprintf(&_dbgf, 120, "[clone] AllocPage FAIL raw=0x%lx used=%lu PP=%d\n",
uint64(pt_pool_raw), pt_pool_used, PT_POOL_PAGES)
serial.puts(&_dbgf)
serial.puts("[clone] AllocPage pml4 failed\n")
return 0
_dbg2: [120]char
viperlib.snprintf(&_dbg2, 120, "[clone] new_pml4=0x%lx\n", uint64(new_pml4_page))
serial.puts(_dbg2)
i: uint64
for i in range(512):
new_pml4_page.entries[i] = pml4.entries[i]
pml4_idx: uint64 = (isolate_vaddr >> 39) & 0x1FF
_dbg3: [120]char
viperlib.snprintf(&_dbg3, 120, "[clone] pml4_idx=%lu entry0=0x%lx\n", pml4_idx, pml4.entries[pml4_idx])
serial.puts(_dbg3)
if not (pml4.entries[pml4_idx] & PTE_PRESENT): return uint64(new_pml4_page)
kernel_pdp: _p* = _p*(pml4.entries[pml4_idx] & ~uint64(0xFFF))
new_pdp_page: _p* = AllocPage()
if not new_pdp_page: return uint64(new_pml4_page)
for i in range(512):
new_pdp_page.entries[i] = kernel_pdp.entries[i]
new_pml4_page.entries[pml4_idx] = (uint64(new_pdp_page) & ~uint64(0xFFF)) | (pml4.entries[pml4_idx] & uint64(0xFFF))
pdp_idx: uint64 = (isolate_vaddr >> 30) & 0x1FF
if not (kernel_pdp.entries[pdp_idx] & PTE_PRESENT): return uint64(new_pml4_page)
# 1GB 大页(PDPTE.PS=1):源端没有下层 PD。先把它拆成常规 PD(语义不变:512 个 2MB
# 条目仍映射同一物理 1GB),否则下面会把**大页内存内容**当 PD 拷进新页表,再用这个
# 垃圾 PD 替换新地址空间的该条目(该区间映射被写坏 / 后续写入落到任意物理内存)。
# 拆完后面沿用既有逐层拷贝逻辑;PD 层命中 2MB 时按既有 `if PTE_PS: return` 早退。
if kernel_pdp.entries[pdp_idx] & PTE_PS:
if _split_1gb_to_pd(uint64(kernel_pdp), pdp_idx) == 0: return uint64(new_pml4_page)
kernel_pd: _p* = _p*(kernel_pdp.entries[pdp_idx] & ~uint64(0xFFF))
new_pd_page: _p* = AllocPage()
if not new_pd_page: return uint64(new_pml4_page)
for i in range(512):
new_pd_page.entries[i] = kernel_pd.entries[i]
new_pdp_page.entries[pdp_idx] = (uint64(new_pd_page) & ~uint64(0xFFF)) | (kernel_pdp.entries[pdp_idx] & uint64(0xFFF))
pd_idx: uint64 = (isolate_vaddr >> 21) & 0x1FF
if not (kernel_pd.entries[pd_idx] & PTE_PRESENT): return uint64(new_pml4_page)
if kernel_pd.entries[pd_idx] & PTE_PS: return uint64(new_pml4_page)
kernel_pt: _p* = _p*(kernel_pd.entries[pd_idx] & ~uint64(0xFFF))
new_pt_page: _p* = AllocPage()
if not new_pt_page: return uint64(new_pml4_page)
for i in range(512):
new_pt_page.entries[i] = kernel_pt.entries[i]
new_pd_page.entries[pd_idx] = (uint64(new_pt_page) & ~uint64(0xFFF)) | (kernel_pd.entries[pd_idx] & uint64(0xFFF))
return uint64(new_pml4_page)
def save_pt_range(vaddr_start: uint64, vaddr_end: uint64, save_buf: uint64*, save_count: uint32*) -> int:
global pml4
if not pml4: return -1
*save_count = uint32(0)
va: uint64 = vaddr_start & ~uint64(0xFFF)
while va < vaddr_end:
pml4_idx: uint64 = (va >> 39) & 0x1FF
if not (pml4.entries[pml4_idx] & PTE_PRESENT): va += 0x1000; continue
pdp_: _p* = _p*(pml4.entries[pml4_idx] & ~uint64(0xFFF))
pdp_idx: uint64 = (va >> 30) & 0x1FF
if not (pdp_.entries[pdp_idx] & PTE_PRESENT): va += 0x1000; continue
# 1GB 大页(PDPTE.PS=1):不是 PD 页表——当 PD 用会把大页内存内容当表项存进保存缓冲
# (后续 restore 会把这些 garbage 写回页表 → 写坏任意内存)。整片跳过(前进 1GB)。
if pdp_.entries[pdp_idx] & PTE_PS: va += 0x40000000; continue
pd_: _p* = _p*(pdp_.entries[pdp_idx] & ~uint64(0xFFF))
pd_idx: uint64 = (va >> 21) & 0x1FF
if not (pd_.entries[pd_idx] & PTE_PRESENT): va += 0x1000; continue
if pd_.entries[pd_idx] & PTE_PS: va += 0x200000; continue
pt_: _p* = _p*(pd_.entries[pd_idx] & ~uint64(0xFFF))
pt_idx: uint64 = (va >> 12) & 0x1FF
saved: uint32 = *save_count
# 记录布局:每条 **16 字节**(va 在 +0、pte 在 +8)。
# 注意:`uint64*` 的 `+ n` 会按元素(8 字节)缩放,所以这里用**显式字节地址**推进记录,
# 字段用元素索引(+1 即 +8 字节)。此前写 `save_buf + saved * 16` 会变成 128 字节步长、
# `+ 8` 会变成 +64 字节,与上面的布局不符(缓冲若按 16 字节/条分配就会越界)。
entry_ptr: uint64* = uint64*(uint64(save_buf) + uint64(saved) * 16)
*(entry_ptr + 0) = va
*(entry_ptr + 1) = pt_.entries[pt_idx]
*save_count = uint32(saved + 1)
va += 0x1000
return 0
def restore_pt_range(save_buf: uint64*, save_count: uint32):
global pml4
if not pml4: return
i: uint32 = 0
while i < save_count:
# 与 save_pt_range 严格一致的布局:每条 **16 字节**(va 在 +0、pte 在 +8);
# 记录用显式字节地址推进,字段用元素索引(+1 即 +8 字节)。两处必须一起改。
entry_ptr: uint64* = uint64*(uint64(save_buf) + uint64(i) * 16)
va: uint64 = *(entry_ptr + 0)
orig_pte: uint64 = *(entry_ptr + 1)
pml4_idx: uint64 = (va >> 39) & 0x1FF
if not (pml4.entries[pml4_idx] & PTE_PRESENT): i += 1; continue
pdp_: _p* = _p*(pml4.entries[pml4_idx] & ~uint64(0xFFF))
pdp_idx: uint64 = (va >> 30) & 0x1FF
if not (pdp_.entries[pdp_idx] & PTE_PRESENT): i += 1; continue
# 1GB 大页(PDPTE.PS=1):save 侧已整片跳过,正常不会有记录落在这里;若出现说明
# 保存缓冲陈旧/错配 —— 同样跳过,绝不把 garbage 基址当 PT 去写回(写坏任意内存)。
if pdp_.entries[pdp_idx] & PTE_PS: i += 1; continue
pd_: _p* = _p*(pdp_.entries[pdp_idx] & ~uint64(0xFFF))
pd_idx: uint64 = (va >> 21) & 0x1FF
if not (pd_.entries[pd_idx] & PTE_PRESENT): i += 1; continue
if pd_.entries[pd_idx] & PTE_PS: i += 1; continue
pt_: _p* = _p*(pd_.entries[pd_idx] & ~uint64(0xFFF))
pt_idx: uint64 = (va >> 12) & 0x1FF
pt_.entries[pt_idx] = orig_pte
__asm__(f"invlpg [{asminp(va, ASM_DESCR.REG_ANY)}]", op=[ASM_DESCR.CLOBBER_MEMORY])
i += 1
def set_active_pml4(cr3_val: uint64):
global pml4
pml4 = _p*(cr3_val & ~uint64(0xFFF))