内存管理-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)    收藏  举报

导航