项目背景
项目名称
openEuler5.10内核支持页表检查功能(page table check)
项目基本需求
(1)合入linux社区中页表检查功能的补丁到openEuler5.10中;
(2)将页表检查功能对arm64的支持也添加到openEuler5.10;
(3)对新特性进行一定的测试,并将合入主线已知的bugfix补丁以及可能存在的其他bug进行修复补丁,整理相关的bugfix合入到openEuler5.10。
项目相关仓库
OpenEuler: https://gitee.com/openeuler/kernel/tree/openEuler-22.09/
项目产生原因
21年11月,一名Google工程师发现linux内核存在一个引用数据下溢(reference count underflow)问题[1-2],而且该问题一直可以追溯到2017年的Linux 4.14内核。该问题会导致无意的页面共享,让内存从一个进程泄露到另一个进程中。为解决此内存缺陷,提出了全新的“页表检查(page table check)”解决方案。
具体问题:https://www.spinics.net/lists/stable/msg515079.html
PERF_SAMPLE_PHYS_ADDR 事件利用 perf_virt_to_phys() 将PMU虚拟地址转化为物理地址。其中利用了两个函数:get_user_page_fast() 以及 page_to_phys()。
在一些情况下,get_user_page_fast_only() 会发生错误并返回false,这表明没有页面引用,但是却仍然用没有页面引用的页去初始化页面指针,在这样的情况下,perf_virt_to_phys() 调用 put_page() 就会导致页面引用下溢,进一步会引起无意识的页面共享。
【疑惑】page sharing的原理,以及page sharing时page 的 _refcount 是怎样的?
在struct page结构中,有非常多的内容,其中之一是_refcount,这是一个atomic_t类型的变量,统计这个page有多少个reference(引用)。atomic_t类型其实就是一个有符号的32bit数,只要_refcount不是0,就说明有对这个page的reference存在,不能把它挪作他用;当_refcount变成0的时候,这个page就能被free了。
技术方法及可行性
该项目的主要工作是将linux社区新增的补丁包(page table check)迁移到openEuler,并进行相关的测试,整理bugfix。此前,我看过linux0.11的相关源码,自己也开发过一个开源的块设备驱动模块,对block layer以及内存管理相关的结构体以及框架比较了解,也熟悉Linux内核开发的流程,可以胜任这个项目。
Linux 内存管理相关
linux的内存管理机制主要是分页机制,即将地址空间等分成一个固定大小的页,而一个页的大小由硬件或者操作系统来决定,目前绝大多数操作系统都以4KB作为页的大小。
(1)linux内存管理中涉及几个重要概念:
- 页:页是将线性地址按固定长度划分得到的,页的内部是连续的地址空间。页是内核进行内存管理的基本单位。
- zone:zone主要用于将页进行逻辑上的分组,但是并没有物理上的意义。比如在x86体系中可以分为3个zone:ZONE_DMA, ZONE_NORMAL, ZONE_HIGHMEM。
- 页框:可以理解成RAM中的页,或者是物理页。页框的大小与一个页的大小一致。页框与页不同,后者只表示一个数据块,可以放在任何页框或者磁盘中。
- 页表:要将页放到页框中,但是连续的页不一定放在连续的页框上,所以就需要一个类似映射表的数据结构,它就是页表。页表负责将线性地址映射到物理地址中。
有了这些概念的铺垫,就能够通过逻辑地址找到物理地址了,linux中主要采用的是多级页表的方式来寻址,这样能够大大节省页表所占用的内存空间。
(2)页的相关操作:
获得页:使用
alloc_pages
接口来获得页1
2
3
4static inline struct page * alloc_pages(gfp_t gfp_mask, unsigned int order)
{
return alloc_pages_current(gfp_mask, order);
}其中,参数gfp_mask标志获得页所使用的行为方式,参数order指定分配多少页(2^order个连续物理页面),返回的指针指向第一个page。
另外还有__get_free_pages,alloc_page以及__get_free_page函数都可以分配页,但本质都是调用alloc_pages来进行的。
释放页:可以使用__free_pages,__free_page,free_pages或者free_page来释放页。
(3)页表相关操作:
页表布局如图所示,linux使用三级页表来进行管理。
每一个进程都有一个指针(mm_struct->pgd)指向它的页面全局目录(PGD),它是一个物理页框。这个页框中包括一个类型为pgd_t的数组。每一个PGD表中的项又指向一个页框,这个页框包含一个类型为pmd_t的数组,这个PMD的每一项指向一个类型为pte_t的页框,这个页框被称为页表项(PTE),这个PTE其实就指向了包含用户数据的那些页框。
使用页表项PTE
pdg_offset(): 由线性地址和mm_struct得到对应的PGD项
1
2pmd_offset(): 由PGD项得到页框地址,再结合一个线性地址得到PMD表中的偏移,进而得到PMD项
1
2pte_offset_kernel(): 由PMD项得到页框地址,再结合一个线性地址得到PTE中的偏移,进而得到PTE项
1
2
3页表检查:
1
2
3
4pte_none(), pmd_none() and pgd_none():如果对应的项不存在,返回1
pte_present(), pmd_present() and pgd_present():如果对应项的PRESENT为被置位,返回1
pte_clear(), pmd_clear() and pgd_clear():会清除对应的项
pmd_bad() and pgd_bad() :检查页表项是否符合要求页表权限检查:
1
2
3
4pte_read():用来测试pte的读权限,pte_mkread() 设置读权限,pte_rdprotect()取消读权限
pte_write():用来测试pte的写权限,pte_mkwrite() 设置读权限,pte_wrprotect()取消读权限
pte_dirty():用来测试是否有被写过,pte_mkdirty()设置dirty位,pte_mkclean()清除dirty位
pte_young():用来测试是否是新页,pte_mkyoung()设置新页,pte_old()设置位旧页(检查access位)
分配和释放页表:
1
2分配:pgd_alloc(), pmd_alloc(), pte_alloc()
释放:pdg_free(), pmd_free(), pte_free()转移和释放页表:
mk_pte(): 输入一个struct page和保护位形成pte_t类型的PTE项
1
2
3set_pte(): 输入一个PMD页框内的地址,然后将PTE项赋值在该地址中
1
pte_page(): 将PTE项转化为struct page
1
Linux内核开发相关
Linux内核大部分由C语言写成,部分会有与汇编语言的联合编程。内核C遵循ISO C89标准,且不依赖于标准C库的支持,不过这也导致浮点运算不被允许等问题。
内核开发的流程包括几个“主内核分支”和很多子系统相关的内核分支,这些分支包括:
- 内核源码树
- 多个主要版本的稳定版内核树
- 子系统相关的内核树
- linux-next集成测试树
同时,对于linux来说,bugzilla.kernel.org是Linux内核开发者们用来跟踪内核Bug的网站,其中,内核源码主目录中的:ref:admin-guide/reporting-bugs.rst
对于修复bug的补丁,应该被恰当地介绍、讨论,并拆成独立的小段。小的补丁不需要太多的时间和精力去验证其正确性,同时也会是调试变得非常容易。
当修复补丁时,需要完全地描述补丁:(1)为什么需要这个修改;(2)补丁的总体设计;(3)实现细节;(4)测试结果。
项目详细方案
将page table check补丁合入openEuler5.10
如图,这里是page table check补丁包的所有改动:
具体的,分成了四个部分,可以在归档中查看每个修改的详细信息:
mm: change page type prior to adding page table entry
在这个修改中,主要修改了添加页表项的时机,在添加页表项之前修改页面类型。主要涉及到改动的文件有:
1
2
3
4
5mm/hugetlb.c | 6 +++---
mm/memory.c | 9 +++++----
mm/migrate.c | 5 ++---
mm/swapfile.c | 4 ++--
4 files changed, 12 insertions(+), 12 deletions(-)在该项目中,可以跟踪这些修改,来对应将它们同步到openEuler5.10中。
mm: ptep_clear() page table helper
在该修改中,添加了一个新函数——ptep_clear(),能够在通用的代码下从页表中删除PTE项。而且后续将利用这个函数与页表检查建立一个联系(hook)。
其中,主要涉及到改动的文件有:
1
2
3
4
5Documentation/vm/arch_pgtable_helpers.rst | 6 ++++--
include/linux/pgtable.h | 8 ++++++++
mm/debug_vm_pgtable.c | 2 +-
mm/khugepaged.c | 12 ++----------
4 files changed, 15 insertions(+), 13 deletions(-)同样地,跟踪这些修改,并同步到openEuler上即可。
mm: page table check
这个修改是核心功能,在页面添加和删除的时候检查用户页表项。并且允许同步捕获与双重映射相关的内存损坏问题。
当一个匿名页的 pte 被添加到页表中时,我们验证这个 pte 还没有指向一个文件支持的页,反之亦然,如果这是一个正在添加的文件支持的页,我们验证这个页没有匿名映射。
我们还强制允许匿名页的只读共享(i.e. cow after fork)。 所有其他共享必须用于文件页。
页表检查允许保护和调试“struct page”元数据由于某种原因损坏的情况。 例如,当 refcnt 或 mapcount 变得无效时。
其中涉及到改动的文件比较多,包括:
1
2
3
4
5
6
7
8
9
10
11
12
13
14Documentation/vm/index.rst | 1 +
Documentation/vm/page_table_check.rst | 56 ++++++
MAINTAINERS | 9 +
arch/Kconfig | 3 +
include/linux/page_table_check.h | 147 ++++++++++++++
mm/Kconfig.debug | 24 +++
mm/Makefile | 1 +
mm/page_alloc.c | 4 +
mm/page_ext.c | 4 +
mm/page_table_check.c | 270 ++++++++++++++++++++++++++
10 files changed, 519 insertions(+)
create mode 100644 Documentation/vm/page_table_check.rst
create mode 100644 include/linux/page_table_check.h
create mode 100644 mm/page_table_check.c跟踪修改,同步支持!
x86: mm: add x86_64 support for page table check
这个修改中,就是完成之前所说的hook了,即将页面表格检查挂钩添加到修改用户页表的例程中。
由于在第二个修改中已经完成了相关功能,所以这里的改动并不多,包括:
1
2
3arch/x86/Kconfig | 1 +
arch/x86/include/asm/pgtable.h | 29 +++++++++++++++++++++++++++--
2 files changed, 28 insertions(+), 2 deletions(-)
将arm64的支持添加到openEuler5.10
这个补丁集进行了一些简单的更改,并使page table check更容易支持新的架构,主要是在ARM64和RISCV上支持此功能。该补丁集的全部改动如下图所示:
包括两个开发者的一共6次修改。
总共的改动其实也不多,可以到归档的文章列表中看到具体每一项的改动信息
同3.1的方法,仔细跟踪补丁集所做的修改,做到细心。
整理相关bugfix合入到openEuler5.10
其实,页表检查这个补丁对应的问题可以追溯到2017年的Linux4.14内核中,发现引用数据下溢会导致内存从一个进程泄露到另一个进程。为解决此类内存缺陷,才引入了page table check。
在邮件列表中已经提到出现过的几个bug了,包括:
可以将这些作为bugfix合入openEuler5.10中,同时,在完成3.1,3.2后我也会进行一些列测试,若发现存在其他bug,也会依照补丁修复的要求来进行bugfix。
项目规划
项目开发第一阶段(7月1日~7月30日)
- 深入熟悉页表相关内核源码,做到心中有数
- 完成需求1:将page table check补丁合入openEuler5.10
- change page type prior to adding page table entry
- ptep_clear() page table helper
- page table check
- add x86_64 support for page table check
- 进行第一阶段测试,确定该补丁正确合入内核
- 对第一阶段任务进行总结,思考可以改进的地方
项目开发第二阶段(8月1日~8月30日)
- 完成需求2:将arm64的支持添加到openEuler5.10
- 进行第二阶段测试
- 对第二阶段任务进行总结,思考可以继续提升的地方
项目开发第三阶段(9月1日~9月30日)
- 整理测试结果
- 整理bugfix相关文档,并合入内核
- 对项目进行总结思考
PR Link
Add page table check for openEuler-22.09 · Pull Request !114 · openEuler/kernel - Gitee.com
参考
- Google提议用“页表检查”功能应对Linux内核的内存崩坏问题
- [[PATCH 5.15 18/20]perf/core: Avoid put_page() when GUP fails - Greg Kroah-Hartman](