正在刊行长文 · Essay
2026-08-30所有内容
随机比特 · Random Bits

一页不可访问区,让越界写更早暴露

2026-08-30AI Engineering / Systemsrbits.uk
一页不可访问区,让越界写更早暴露

本机 Linux 两页实验里,第一页的最后一个字节是 offset 4095,写它没有问题。再往后一字节,offset 4096 已经进入第二页:同一组 Linux 映射的第二页普通可写时,这次越界把用于观察改写的 sentinel(哨兵字节)从 0xA5 改成 0x5A,没有信号,子进程正常退出;第二页设为 PROT_NONE 后,同一落点触发 SIGSEGV / SEGV_ACCERR,故障地址相对映射起点的偏移为 4096,写入没有完成。

两次危险写都在 fork 子进程的匿名私有映射中执行。基线写受写时复制隔离,没有改动父进程视图;信号处理器也只记录结果并立即退出,不把段错误当成可以继续业务的恢复协议。

不可访问页将静默写入转换为故障

可以把两页地址想成走廊里相邻的两间房。第一页容纳逻辑缓冲区,第二页仍可写时,越过门槛的指针只是走进另一间开放房间,操作系统不知道它已经越过数组边界。Linux 实验用 mprotect(..., PROT_NONE) 锁住第二页后,这一页才成为 Guard Page,也就是一页不能访问的地址区。

本机 Linux 页大小是 4096 字节,所以 4095 仍属于第一页,4096 刚好落在禁区起点。实验恢复第二页读写权限后,4096 又能写入。Linux 实验的两组都对 offset 4096 发起单字节写入,关键差别是第二页的访问权限。

同一 offset 4096 在普通可写页和 PROT_NONE 页中的不同结果;offset 4095 仍可写

同一个越界落点,页面权限不同,终态也不同。

故障地址因此比一个笼统的“程序异常终止”更有用:它能核对访问是否真的落进了预先布置的禁区,而不是把任何段错误都归功于 Guard Page。

保护范围只覆盖真正命中的访问

Guard Page 不知道数组有多长,它只知道某个虚拟页能不能访问。对象末端若停在可写页中间,小幅越界仍可能留在同一页;错误指针若一次跳过整页禁区,落到另一段可写映射,也可能没有触发这道陷阱。因此,保护效果首先取决于布局:对象边界要贴近页边界,错误地址还必须真正落入保护区。

xv6 开放书稿在用户栈下方和内核栈映射之间安排 guard 区,意图是让命中的溢出更早 page fault 或 panic,而不是继续覆盖别处。这说明了早失败的价值,却不能代替 Linux 或 Windows 的实现证明。

Guard Page 买到的不是绝对安全,而是把命中保护区的一部分静默越界,换成更早、更可定位的失败。

本次实验确认的是一页大小的虚拟地址区间及其访问保护。它没有测量物理 RAM、RSS、页表或 VMA、TLB 与性能;xv6 的一种实现甚至先分配页再撤掉用户访问权,所以“固定浪费一页物理内存”和“完全不占物理内存”都不是通用结论。

Linux 与 Windows 使用不同保护契约

Linux 实验中的 PROT_NONE 在权限被显式改回前持续拒绝访问。Windows 官方文档中的 PAGE_GUARD 则是附加在底层页面保护上的一次性告警;首次访问产生 STATUS_GUARD_PAGE_VIOLATION 后,guard 标志会被清除,之后能否访问由底层保护决定。它也不能和 PAGE_NOACCESS 组合。本轮没有 Windows 实测,不能把这段官方契约写成本机复现。

Linux PROT_NONE 持续不可访问与 Windows PAGE_GUARD 首次告警后清除的生命周期对照

同叫保护页,持续时间、异常类型和重新布防责任并不相同。

工程验收要把三件事连起来看:先让被保护对象的末端与页面布局匹配,再用固定越界落点断言异常类型、相对故障地址和写入终态,最后核对保护是持续还是一次性,并把地址空间、物理驻留、内核元数据与性能分别测量。线程栈、调试分配器、运行时栈和隔离共享内存都能采用这种思路,但不能把所有内存安全问题交给一页禁区。

规范依据来自 Linux man-pages 6.19 的 mprotect(2)xv6-riscv 开放书稿固定版本Microsoft Creating Guard Pages

随机比特公众号二维码
公众号 · 随机比特
从 AI 工具热闹里拆工程真相

写边界、控制面、上下文、成本与安全。