根因: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 装载,便于隔离内核问题)。
591 lines
29 KiB
Plaintext
591 lines
29 KiB
Plaintext
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))
|