import drivers.serial.uart.serial as serial import viperlib import string 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) # 4KB PTE bit 7 = PAT select (for large pages the same bit is PS). define PTE_PAT = (1 << 7) # PAT entry 4 is reprogrammed to write-combining (WC) for the framebuffer. # IA32_PAT is configured in main.vp after CPUID tells us PAT is available. _pat_wc: int = 0 def set_pat_wc(): global _pat_wc _pat_wc = 1 def has_pat_wc() -> int: return _pat_wc 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 # 内核启动时的 PML4;运行时 clone 新进程必须以它为模板,不能克隆调用者(父进程) # 的用户页表,否则父子进程会共享 heap/stack 的 PT/PTE。 _kernel_pml4: uint64 = 0 pdp: _p* pd: _p* pt: _p* def init(MemmapAddr: uint64, MemmapSize: uint64): global pml4, _kernel_pml4 pml4 = GetCurrentPml4() _kernel_pml4 = uint64(pml4) # 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) # 只继承缓存/执行属性,**绝不继承 PTE_USER**:这些 2MB 条目是 identity 映射, # 用户态只能通过 MapPage 为具体 4KB 页显式加 USER。若把固件 1GB 条目的 U=1 # 带进 2MB 叶条目,应用一旦越界写到尚未映射的 VA(例如 heap 0x21780000), # 就会命中 identity 叶页、直接改物理内存里的 buddy 块头(BAD blk 源头)。 inherit: uint64 = e & (PTE_PCD | PTE_PWT | 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 由 MapPage 在真正建立 # 用户页时逐级补上。非叶条目也不能带 bit63(NX)(见下)。 pdp_t.entries[pdp_idx] = (uint64(new_pd) & ~uint64(0xFFF)) | PTE_PRESENT | PTE_WRITABLE return 1 # 把 1GiB 范围内"物理地址 == 虚拟地址"的 identity 叶条目的 USER 位清掉。 # 参数 gb_base 必须是 1GiB 对齐的虚拟基址(pdp_idx << 30)。 def _sanitize_identity_pd(pd: _p*, gb_base: uint64): i: int = 0 while i < 512: e: uint64 = pd.entries[i] if (e & PTE_PRESENT) == 0: i += 1 continue va2m: uint64 = gb_base + uint64(i) * uint64(0x200000) if (e & PTE_PS) != 0: if (e & ~uint64(0x1FFFFF)) == va2m and (e & PTE_USER) != 0: pd.entries[i] = e & ~PTE_USER else: pt_s: _p* = _p*(e & ~uint64(0xFFF)) j: int = 0 while j < 512: ee: uint64 = pt_s.entries[j] if (ee & PTE_PRESENT) != 0 and (ee & PTE_USER) != 0: va4k: uint64 = va2m + uint64(j) * uint64(0x1000) if (ee & ~uint64(0xFFF)) == va4k: pt_s.entries[j] = ee & ~PTE_USER j += 1 i += 1 # 为 dst 进程克隆 vaddr 所在的 PML4/PDP/PD/PT 分支。已克隆过的层级直接复用, # 因此可以对同一 1GiB 内的多个 2MiB 区域连续调用。 # 关键用途:用户栈都映射在同一个 USER_STACK_VTOP;若不把该分支私有化, # 新进程映射自己的栈会改写共享页表,把父进程的栈 PTE 覆盖掉 → ret 到 0。 def _clone_vaddr(dst: _p*, vaddr: uint64): pml4_i: uint64 = (vaddr >> 39) & 0x1FF if not (pml4.entries[pml4_i] & PTE_PRESENT): return src_pdp: _p* = _p*(pml4.entries[pml4_i] & ~uint64(0xFFF)) dst_pdp_ent: uint64 = dst.entries[pml4_i] if dst_pdp_ent == pml4.entries[pml4_i]: new_pdp: _p* = AllocPage() if not new_pdp: return i: int = 0 while i < 512: new_pdp.entries[i] = src_pdp.entries[i] if (new_pdp.entries[i] & PTE_PRESENT) != 0 and (new_pdp.entries[i] & PTE_PS) != 0: new_pdp.entries[i] = new_pdp.entries[i] & ~PTE_USER i += 1 dst.entries[pml4_i] = (uint64(new_pdp) & ~uint64(0xFFF)) | (pml4.entries[pml4_i] & uint64(0xFFF) & ~PTE_USER) dst_pdp_ent = dst.entries[pml4_i] dst_pdp: _p* = _p*(dst_pdp_ent & ~uint64(0xFFF)) pdp_i: uint64 = (vaddr >> 30) & 0x1FF if not (src_pdp.entries[pdp_i] & PTE_PRESENT): return if src_pdp.entries[pdp_i] & PTE_PS: if _split_1gb_to_pd(uint64(src_pdp), pdp_i) == 0: return src_pd: _p* = _p*(src_pdp.entries[pdp_i] & ~uint64(0xFFF)) if dst_pdp.entries[pdp_i] == src_pdp.entries[pdp_i]: new_pd: _p* = AllocPage() if not new_pd: return i = 0 while i < 512: new_pd.entries[i] = src_pd.entries[i] if (new_pd.entries[i] & PTE_PRESENT) != 0 and (new_pd.entries[i] & PTE_PS) != 0: new_pd.entries[i] = new_pd.entries[i] & ~PTE_USER i += 1 dst_pdp.entries[pdp_i] = (uint64(new_pd) & ~uint64(0xFFF)) | (src_pdp.entries[pdp_i] & uint64(0xFFF) & ~PTE_USER) dst_pd: _p* = _p*(dst_pdp.entries[pdp_i] & ~uint64(0xFFF)) pd_i: uint64 = (vaddr >> 21) & 0x1FF if not (src_pd.entries[pd_i] & PTE_PRESENT): return if src_pd.entries[pd_i] & PTE_PS: return if dst_pd.entries[pd_i] == src_pd.entries[pd_i]: src_pt: _p* = _p*(src_pd.entries[pd_i] & ~uint64(0xFFF)) new_pt: _p* = AllocPage() if not new_pt: return i = 0 while i < 512: new_pt.entries[i] = src_pt.entries[i] & ~PTE_USER i += 1 dst_pd.entries[pd_i] = (uint64(new_pt) & ~uint64(0xFFF)) | (src_pd.entries[pd_i] & uint64(0xFFF) & ~PTE_USER) 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)}") # Physical address backing a 4KB user page, or 0 if unmapped / a huge page. # munmap() needs this before UnMapPage() clears the PTE, so it can hand the # frame back to the buddy allocator. # (Local helper: _table_phys is defined later in this module and Viper does # not allow same-module forward references.) def _entry_table(e: uint64) -> uint64: if (e & PTE_PRESENT) == 0: return 0 if (e & uint64(0x7FF0000000000000)) != 0: return 0 return e & uint64(0x000FFFFFFFFFF000) def GetPagePhys(VirtAddr: uint64) -> uint64: if VirtAddr == 0: return 0 p4: _p* = pml4 if not p4: return 0 va: uint64 = VirtAddr & ~uint64(0xFFF) i4: uint64 = (va >> 39) & 0x1FF pdp_pa: uint64 = _entry_table(p4.entries[i4]) if pdp_pa == 0: return 0 p3: _p* = _p*(pdp_pa) i3: uint64 = (va >> 30) & 0x1FF if p3.entries[i3] & PTE_PS: return 0 pd_pa: uint64 = _entry_table(p3.entries[i3]) if pd_pa == 0: return 0 p2: _p* = _p*(pd_pa) i2: uint64 = (va >> 21) & 0x1FF if p2.entries[i2] & PTE_PS: return 0 pt_pa: uint64 = _entry_table(p2.entries[i2]) if pt_pa == 0: return 0 p1: _p* = _p*(pt_pa) i1: uint64 = (va >> 12) & 0x1FF e: uint64 = p1.entries[i1] if not (e & PTE_PRESENT): return 0 return e & uint64(0x000FFFFFFFFFF000) # Rewrite the protection bits of an existing 4KB mapping, keeping the frame. # Returns 1 on success, 0 if the page is not a present 4KB page. def SetPageFlags(VirtAddr: uint64, flags: uint64) -> int: if VirtAddr == 0: return 0 p4: _p* = pml4 if not p4: return 0 va: uint64 = VirtAddr & ~uint64(0xFFF) i4: uint64 = (va >> 39) & 0x1FF pdp_pa: uint64 = _entry_table(p4.entries[i4]) if pdp_pa == 0: return 0 p3: _p* = _p*(pdp_pa) i3: uint64 = (va >> 30) & 0x1FF if p3.entries[i3] & PTE_PS: return 0 pd_pa: uint64 = _entry_table(p3.entries[i3]) if pd_pa == 0: return 0 p2: _p* = _p*(pd_pa) i2: uint64 = (va >> 21) & 0x1FF if p2.entries[i2] & PTE_PS: return 0 pt_pa: uint64 = _entry_table(p2.entries[i2]) if pt_pa == 0: return 0 p1: _p* = _p*(pt_pa) i1: uint64 = (va >> 12) & 0x1FF e: uint64 = p1.entries[i1] if not (e & PTE_PRESENT): return 0 p1.entries[i1] = (e & uint64(0x000FFFFFFFFFF000)) | flags | PTE_PRESENT __asm__(f"invlpg [{asminp(va, ASM_DESCR.REG_ANY)}]") return 1 # 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) 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 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 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_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 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_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(): """Make all page-table pages currently reachable from pml4 writable. 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): 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)) for pj in range(512): if pdp_m.entries[pj] & PTE_PS: continue 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)) for pk in range(512): if pd_m.entries[pk] & PTE_PS: continue 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 位(只读) 定位越界写源:内核态写只读页在 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, stack_vaddr: uint64 = 0) -> 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] # 固件 identity 大页可能带 U=1:克隆到进程地址空间后必须降为 supervisor。 # 用户页的 USER 位由后续 MapPage 在具体 4KB PTE 上显式添加。 if (new_pdp_page.entries[i] & PTE_PRESENT) != 0 and (new_pdp_page.entries[i] & PTE_PS) != 0: new_pdp_page.entries[i] = new_pdp_page.entries[i] & ~PTE_USER new_pml4_page.entries[pml4_idx] = (uint64(new_pdp_page) & ~uint64(0xFFF)) | (pml4.entries[pml4_idx] & uint64(0xFFF) & ~PTE_USER) 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)) # 固件 identity 映射可能把 4KB/2MB 叶条目的 U 置 1。只要 U=1,用户态一旦 # 写到尚未显式映射的 VA(应用越界写),就会直接落到**同数值物理页**上—— # buddy 块头、页表、别人的数据全会被踩(现场:1GiB 下 0x21780000 的 # order-16 空闲块头被清零)。这里把该 1GiB 内"物理地址 == 虚拟地址"的 # identity 叶条目降为 supervisor;真正的用户页(phys != va)保持原样。 _sanitize_identity_pd(kernel_pd, isolate_vaddr & ~uint64(0x3FFFFFFF)) 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] if (new_pd_page.entries[i] & PTE_PRESENT) != 0 and (new_pd_page.entries[i] & PTE_PS) != 0: new_pd_page.entries[i] = new_pd_page.entries[i] & ~PTE_USER new_pdp_page.entries[pdp_idx] = (uint64(new_pd_page) & ~uint64(0xFFF)) | (kernel_pdp.entries[pdp_idx] & uint64(0xFFF) & ~PTE_USER) 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] & ~PTE_USER new_pd_page.entries[pd_idx] = (uint64(new_pt_page) & ~uint64(0xFFF)) | (kernel_pd.entries[pd_idx] & uint64(0xFFF) & ~PTE_USER) # 以 kernel PML4 为模板时,PML4[511](用户栈所在高半区)通常没有映射; # 之后 _elf_entry_wrapper 里的 MapPage 会为该进程新建私有 PDP/PD/PT。 # 不要在这里额外 clone 高半区:kernel PML4[511] 可能含有 1GiB/大页或 # 非用户映射,强行拆分会在 clone 阶段访问用户栈 VA 触发内核 #PF。 return uint64(new_pml4_page) # Deep-copy every USER mapping from src into dst (dst must already contain the # kernel mappings, e.g. from create_process_page_table()). Data frames are # allocated through alloc_fn(4096); page-table pages come from the paging pool. # # A table is only duplicated when its subtree really contains user pages; # otherwise the parent's table is shared. Without that check a single user # page at 0x400000 drags the whole kernel identity map (one PT page per 2MB # region) into the new address space and exhausts the page-table pool. def _alloc_user_frame(alloc_fn: rawptr) -> rawptr: newp: rawptr = None __asm__(f"""mov r11, {asminp(alloc_fn, ASM_DESCR.REG_ANY)} push rbp mov rbp, rsp and rsp, -16 mov rdi, 4096 call r11 mov rsp, rbp pop rbp mov {asmout(newp, ASM_DESCR.OUTPUT_REG)}, rax""", op=[ASM_DESCR.CLOBBER_MEMORY, ASM_DESCR.CLOBBER_RAX, ASM_DESCR.CLOBBER_RCX, ASM_DESCR.CLOBBER_RDX, ASM_DESCR.CLOBBER_RSI, ASM_DESCR.CLOBBER_RDI, ASM_DESCR.CLOBBER_R8, ASM_DESCR.CLOBBER_R9, ASM_DESCR.CLOBBER_R10, ASM_DESCR.CLOBBER_R11]) return newp # level: 1 = PT, 2 = PD, 3 = PDPT (4 = PML4 only used as the entry point). def _table_has_user(t: _p*, level: int) -> int: i: int = 0 while i < 512: e: uint64 = t.entries[i] if (e & PTE_PRESENT) != 0: if level == 1: if (e & PTE_USER) != 0: return 1 elif (e & PTE_PS) != 0: if (e & PTE_USER) != 0: return 1 elif (e & PTE_USER) != 0: if _table_has_user(_p*(e & ~uint64(0xFFF)), level - 1) != 0: return 1 i += 1 return 0 def _copy_user_table(src: _p*, dst: _p*, level: int, alloc_fn: rawptr) -> int: i: int = 0 while i < 512: e: uint64 = src.entries[i] if (e & PTE_PRESENT) == 0: dst.entries[i] = e elif level == 1: if (e & PTE_USER) == 0: dst.entries[i] = e else: src_phys: uint64 = e & uint64(0x000FFFFFFFFFF000) newp: rawptr = _alloc_user_frame(alloc_fn) if newp is None: return -1 string.memcpy(newp, rawptr(src_phys), 4096) dst.entries[i] = (uint64(newp) & ~uint64(0xFFF)) | (e & uint64(0xFFF)) elif (e & PTE_PS) != 0: dst.entries[i] = e elif (e & PTE_USER) == 0: dst.entries[i] = e else: csrc: _p* = _p*(e & ~uint64(0xFFF)) if _table_has_user(csrc, level - 1) == 0: dst.entries[i] = e else: np: _p* = AllocPage() if not np: return -1 dst.entries[i] = (uint64(np) & ~uint64(0xFFF)) | (e & uint64(0xFFF)) if _copy_user_table(csrc, np, level - 1, alloc_fn) != 0: return -1 i += 1 return 0 def CopyUserSpace(src_pa: uint64, dst_pa: uint64, alloc_fn: rawptr) -> int: if src_pa == 0 or dst_pa == 0: return -1 s4: _p* = _p*(src_pa) d4: _p* = _p*(dst_pa) return _copy_user_table(s4, d4, 4, alloc_fn) 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)) export def get_kernel_pml4() -> uint64: return _kernel_pml4