内存管理-74-page_table_check-1-理论
主要文件:
//kernel-6.1/mm/Kconfig.debug //kernel-6.1/Documentation/translations/zh_CN/mm/page_table_check.rst //kernel-6.1/mm/page_table_check.c
一、CONFIG_PAGE_TABLE_CHECK 简介
1. 一句话概括
CONFIG_PAGE_TABLE_CHECK 是一套运行时物理页映射合法性验证机制,为每个物理页维护一组引用计数器,在每次 PTE/PMD/PUD 被设置或清除时检查页表操作的正确性,能在第一时间捕获内核中因错误导致的"非法页表映射"问题。
2. 要解决什么问题
内核中存在各种可能产生非法页表映射的 bug:
------------------------------------------------------------------------------------------- 异常场景 后果 ------------------------------------------------------------------------------------------- 匿名页以可写映射被同时映射到两个不同进程 进程间数据泄漏、corruption 同一物理页同时被映射为匿名页和文件页 内核数据结构混乱 页已被释放(归还 buddy)但仍被某个页表引用 use-after-free,最严重的安全漏洞之一 页正在被分配,但仍被旧页表引用 数据篡改 -------------------------------------------------------------------------------------------
这些 bug 非常隐蔽,可能在发生很久之后才被观察到(数据静默损坏、难以复现的崩溃),难以定位到根因。PAGE_TABLE_CHECK 在映射发生/消失的那一刻就进行验证,将问题暴露在第一现场。
3. 核心数据结构
struct page_table_check { atomic_t anon_map_count; /* 作为匿名页被用户态页表映射的次数 */ atomic_t file_map_count; /* 作为文件页被用户态页表映射的次数 */ };
通过 page_ext (每个物理页的扩展元数据区)存储,每个物理页对应一个 page_table_check 结构。只统计用户态可访问的映射(见 pte_user_accessible_page),内核自身的映射(init_mm)被排除。
4. 工作原理
(1) 页分配/释放时:page_table_check_alloc() / page_table_check_free()
void __page_table_check_zero(struct page *page, unsigned int order) { for (i = 0; i < (1 << order); i++) { BUG_ON(atomic_read(&ptc->anon_map_count)); //分配时 map count 必须为 0 BUG_ON(atomic_read(&ptc->file_map_count)); //否则说明"被释放的页仍被映射着" } }
在 post_alloc_hook (分配时) 和 free_pages_prepare (释放时)调用。不变量:空闲页/新分配页的 anon_map_count 和 file_map_count 必须都为 0,违反 BUG_ON 触发内核 panic,立即暴露问题。
(2) 页表建立映射时:page_table_check_pte_set:
void __page_table_check_pte_set(mm, addr, ptep, pte) { //先处理旧 PTE (清除旧映射的计数) __page_table_check_pte_clear(mm, addr, *ptep); //对新 PTE 增加计数 if (pte_user_accessible_page(pte)) { page_table_check_set(mm, addr, pte_pfn(pte), pgcnt, pte_write(pte)); } }
page_table_check_set 的核心检查:
if (anon) { BUG_ON(atomic_read(&ptc->file_map_count)); //匿名页不应同时有文件映射 /* 关键:匿名页以可写映射被多个进程同时映射 --> BUG! (只读映射可多次映射,如 COW 之前的共享) */ BUG_ON(atomic_inc_return(&ptc->anon_map_count) > 1 && rw); } else { BUG_ON(atomic_read(&ptc->anon_map_count)); //文件页不应同时有匿名映射 BUG_ON(atomic_inc_return(&ptc->file_map_count) < 0); //计数不应下溢 }
(3) 页表清除映射时:page_table_check_pte_clear:
if (anon) { BUG_ON(atomic_read(&ptc->file_map_count)); //类型一致性检查 BUG_ON(atomic_dec_return(&ptc->anon_map_count) < 0); //计数不应为负 } else { BUG_ON(atomic_read(&ptc->anon_map_count)); BUG_ON(atomic_dec_return(&ptc->file_map_count) < 0); }
5. 检查的五条不变量
----------------------------------------------------------------------------------------------------- # 不变量 违反意味着 ----------------------------------------------------------------------------------------------------- 1 分配/释放时 map_count 必须为 0 释放了仍被映射的页,或分配到了"不干净"的页 2 同一物理页不能同时有 anon_map 和 file_map 同一物理帧被错误地既当匿名页又当文件页使用 3 匿名页以可写方式映射到多个进程 COW 机制失败,数据将在进程间不隔离地泄漏 4 map_count 不能为负 多次清除了同一个映射(double-unmap) 5 仅检查用户态映射,跳过 init_mm 避免内核直接映射(linear map)的误报 -----------------------------------------------------------------------------------------------------
覆盖的页表层级:
----------------------------------------------------------------------------------------------------- API 映射粒度 场景 ----------------------------------------------------------------------------------------------------- page_table_check_pte_set/clear 4KB(普通页) 正常页缺页、COW、munmap 等 page_table_check_pmd_set/clear 2MB(PMD 透明大页) THP 分配/释放/split page_table_check_pud_set/clear 1GB(PUD 大页) HugeTLB 1G 页 page_table_check_pte_clear_range 批量 4KB zap_pte_range 等批量释放 -----------------------------------------------------------------------------------------------------
6. 性能与使用方式
(1) 开销:
a. 每个物理页额外占用 sizeof(struct page_table_check) = 8 字节(2 × atomic_t);
b. 每次 PTE set/clear 操作增加一次 atomic 操作 + 条件检查;
c. 通过 static_key (jump label)实现零开销关闭:
if (static_branch_likely(&page_table_check_disabled)) return; //编译为 NOP,不执行任何检查
(2) 启用方式:
------------------------------------------------------------------------ 方式 说明 ------------------------------------------------------------------------ 编译时强制开启 CONFIG_PAGE_TABLE_CHECK_ENFORCED=y 运行时内核参数 page_table_check=1, 启动参数,灵活控制 默认状态 编译了但不执行(static_key disabled,性能零影响) ------------------------------------------------------------------------
在页面分配器中的位置:
post_alloc_hook() //page_alloc.c │ ├─ set_page_private(0) ├─ set_page_refcounted() ├─ arch_alloc_page() ├─ debug_pagealloc_map_pages() ├─ kernel_unpoison_pages() ├─ [KASAN 处理] ├─ kernel_init_pages() ├─ set_page_owner() └─ page_table_check_alloc(page, order) //验证分配的页 map_count == 0 free_pages_prepare() //page_alloc.c │ ├─ page_table_check_free(page, order) //验证释放的页 map_count == 0 ├─ ...
在页表操作层面:
set_pte_at(mm, addr, ptep, pte) // arch/*/include/asm/pgtable.h │ └─ page_table_check_pte_set(mm, addr, ptep, pte) //每次写 PTE 时检查 pte_clear(mm, addr, ptep) │ └─ page_table_check_pte_clear(mm, addr, oldpte) //每次清 PTE 时检查
7. 能捕获的典型 bug 类型
(1) COW 实现缺陷:fork 后匿名页本应 read-only 共享,但某处代码错误地以可写方式映射到子进程 ==> anon_map_count > 1 && rw 触发 BUG_ON().
(2) Double mapping:内核 bug 导致同一物理页同时出现在两个不相关进程的页表中且都可写 ==> 数据损坏的前兆,被立即捕获。
(3) Use-after-free:页面已归还 buddy(map_count 应为 0),但某处代码仍持有指向它的 PTE ==> 再次分配该页时 page_table_check_alloc() 发现 map_count ≠ 0,BUG_ON().
(4) 类型混淆:某处代码错误地将文件页当匿名页映射(或反之)==> BUG_ON(atomic_read(&ptc->file_map_count)) 在匿名映射路径触发。
(5) Double-unmap:同一个 PTE 被清除了两次 ==> atomic_dec_return < 0,计数下溢触发 BUG_ON().
8. 与其他调试工具的对比
-------------------------------------------------------------------------------------------------------------- 工具 检测目标 开销 覆盖范围 -------------------------------------------------------------------------------------------------------------- PAGE_TABLE_CHECK 页表映射合法性 低(原子操作+static key) 所有用户态页表操作 DEBUG_PAGEALLOC 释放后非法访问(unmap 保护) 高(频繁 TLB flush) 所有页面分配/释放 KASAN 堆/栈越界、use-after-free 高(影子内存 1/8 开销) 所有内存访问 PAGE_OWNER 分配来源追踪(内存泄漏) 中(记录调用栈) 所有页面分配 PAGE_POISONING 释放后内容检测 中(填充毒化字节) 所有页面释放 --------------------------------------------------------------------------------------------------------------
PAGE_TABLE_CHECK 的独特价值:它是唯一能在页表层面检测映射合法性的工具,其他工具要么关注内存内容,要么关注分配/释放生命周期,都无法在 PTE 被错误修改的那一刻立即报警。
二、补充
1. 匿名页+可写+被多进程映射为啥非法
linux 中"可写 + 被多个进程映射"的物理页只有一种合法情况:共享内存(shared mapping)。但共享内存页不是匿名页(PageAnon 为 false),它是文件页。共享内存底层实现其实都是文件页:
------------------------------------------------------------------------------------- 共享内存方式 底层实现 page->mapping 指向 ------------------------------------------------------------------------------------- shmget/shmat(System V 共享内存) tmpfs 文件 tmpfs 的 address_space mmap(MAP_SHARED, fd) 对应文件 文件的 address_space `mmap(MAP_SHARED MAP_ANONYMOUS)` 匿名 tmpfs 文件(/dev/zero) memfd_create + mmap tmpfs 文件 tmpfs 的 address_space -------------------------------------------------------------------------------------
即使你用了 MAP_ANONYMOUS | MAP_SHARED,内核也会在内部创建一个 tmpfs 匿名文件来作为后端,最终的物理页仍然是文件页(PageAnon == false)。
(1) 匿名页可写时不允许被多个进程映射的原因:
匿名页的"共享"只有一种合法场景:fork 之后、COW 之前的只读共享。
fork 之后: 父进程 PTE --> [物理页 P] (只读) 子进程 PTE --> [物理页 P] (只读) anon_map_count = 2,rw = false --> 合法(COW 保护中) 子进程写入触发 COW: 子进程 PTE --> [新物理页 P'](可写) 父进程 PTE --> [物理页 P] (可写/只读) 原页 P 的 anon_map_count 降为 1
一旦 COW 完成,每个进程持有自己的物理副本,不再共享。这是进程隔离的基础保证。
如果一个匿名页可写地被两个进程映射,意味着一个进程写入数据时另一个进程能看到 —— 这破坏了进程间的私有内存隔离。这不是"设计选择",而是"安全保证"。如果两个进程需要可写地共享内存,必须显式使用共享内存接口(shmem/mmap MAP_SHARED),此时内核分配的是文件页(tmpfs),不是匿名页。
(2) 汇总对比
------------------------------------------------------------------------------------------------ 匿名页(PageAnon) 文件页/共享内存页(!PageAnon) ------------------------------------------------------------------------------------------------ 多进程只读映射 ✓(fork COW 前) ✓(正常) 多进程可写映射 ✗ 非法!BUG_ON! ✓(共享内存的正常语义) page->mapping 指向 anon_vma 指向 address_space(文件/tmpfs) rmap 路径 anon_vma 链 i_mmap tree 对应用户接口 malloc/brk/MAP_PRIVATE mmap MAP_SHARED / shmget ------------------------------------------------------------------------------------------------
(3) 具体代码验证路径
//page_table_check_set() 中: anon = PageAnon(page); if (anon) { //匿名页 /* 匿名页绝不应同时有文件映射 */ BUG_ON(atomic_read(&ptc->file_map_count)); /* 匿名页 + 可写 + 被 >1 个映射 = COW 机制出了问题! */ BUG_ON(atomic_inc_return(&ptc->anon_map_count) > 1 && rw); } else { //文件页(包括共享内存、tmpfs) /* 文件页绝不应同时有匿名映射 */ BUG_ON(atomic_read(&ptc->anon_map_count)); /* 只检查不下溢, 不限制映射次数和可写性, 共享内存多进程可写完全合法 */ BUG_ON(atomic_inc_return(&ptc->file_map_count) < 0); }
(4) 一句话总结
共享内存在内核中是"文件页"(由 tmpfs 支撑),不是"匿名页"。PAGE_TABLE_CHECK 只禁止匿名页被多个进程可写共享 —— 这代表 COW 机制失效,是一个安全漏洞;而文件页被多进程可写共享是共享内存的正常语义,完全允许。
posted on 2026-08-04 22:10 Hello-World3 阅读(6) 评论(0) 收藏 举报
浙公网安备 33010602011771号