项目背景

项目名称

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内存管理中涉及几个重要概念:

  1. :页是将线性地址按固定长度划分得到的,页的内部是连续的地址空间。页是内核进行内存管理的基本单位。
  2. zone:zone主要用于将页进行逻辑上的分组,但是并没有物理上的意义。比如在x86体系中可以分为3个zone:ZONE_DMA, ZONE_NORMAL, ZONE_HIGHMEM。
  3. 页框:可以理解成RAM中的页,或者是物理页。页框的大小与一个页的大小一致。页框与页不同,后者只表示一个数据块,可以放在任何页框或者磁盘中。
  4. 页表:要将页放到页框中,但是连续的页不一定放在连续的页框上,所以就需要一个类似映射表的数据结构,它就是页表。页表负责将线性地址映射到物理地址中。

有了这些概念的铺垫,就能够通过逻辑地址找到物理地址了,linux中主要采用的是多级页表的方式来寻址,这样能够大大节省页表所占用的内存空间。

(2)页的相关操作:

  1. 获得页:使用alloc_pages接口来获得页

    1
    2
    3
    4
    static 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来进行的。

  2. 释放页:可以使用__free_pages,__free_page,free_pages或者free_page来释放页。

(3)页表相关操作:

页表结构

页表布局如图所示,linux使用三级页表来进行管理。

每一个进程都有一个指针(mm_struct->pgd)指向它的页面全局目录(PGD),它是一个物理页框。这个页框中包括一个类型为pgd_t的数组。每一个PGD表中的项又指向一个页框,这个页框包含一个类型为pmd_t的数组,这个PMD的每一项指向一个类型为pte_t的页框,这个页框被称为页表项(PTE),这个PTE其实就指向了包含用户数据的那些页框。

  1. 使用页表项PTE

    • pdg_offset(): 由线性地址和mm_struct得到对应的PGD项

      1
      2
      #define pgd_offset(mm, address) ((mm)->pgd+pgd_index(address))
      #define pgd_index(address) (((address) >> PGDIR_SHIFT) & (PTRS_PER_PGD-1))
    • pmd_offset(): 由PGD项得到页框地址,再结合一个线性地址得到PMD表中的偏移,进而得到PMD项

      1
      2
      #define pmd_offset(dir, address) ((pmd_t *) pgd_page(*(dir)) + pmd_index(address))
      #define pgd_page(pgd) ((unsigned long) __va(pgd_val(pgd) & PAGE_MASK))
    • pte_offset_kernel(): 由PMD项得到页框地址,再结合一个线性地址得到PTE中的偏移,进而得到PTE项

      1
      2
      3
      #define pte_offset_kernel(pmd, address) ((pte_t *) pmd_page_kernel(*(pmd)) +  pte_index(address))
      #define pmd_page_kernel(pmd) ((unsigned long) __va(pmd_val(pmd) & PAGE_MASK))
      #define pte_index(address) (((address) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))
    • 页表检查:

      1
      2
      3
      4
      pte_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
      4
      pte_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位)
  2. 分配和释放页表:

    1
    2
    分配:pgd_alloc(), pmd_alloc(), pte_alloc()
    释放:pdg_free(), pmd_free(), pte_free()
  3. 转移和释放页表:

    • mk_pte(): 输入一个struct page和保护位形成pte_t类型的PTE项

      1
      2
      3
      #define page_to_pfn(page)	((unsigned long)((page) - mem_map)) 
      #define pfn_pte(pfn, prot) __pte(((pfn) << PAGE_SHIFT) | pgprot_val(prot))
      #define mk_pte(page, pgprot) pfn_pte(page_to_pfn(page), (pgprot))
    • set_pte(): 输入一个PMD页框内的地址,然后将PTE项赋值在该地址中

      1
      #define set_pte(pteptr, pteval) (*(pteptr) = pteval)
    • pte_page(): 将PTE项转化为struct page

      1
      #define pte_page(pte)	pfn_to_page(pte_pfn(pte))

Linux内核开发相关

Linux内核大部分由C语言写成,部分会有与汇编语言的联合编程。内核C遵循ISO C89标准,且不依赖于标准C库的支持,不过这也导致浮点运算不被允许等问题。

内核开发的流程包括几个“主内核分支”和很多子系统相关的内核分支,这些分支包括:

  • 内核源码树
  • 多个主要版本的稳定版内核树
  • 子系统相关的内核树
  • linux-next集成测试树

同时,对于linux来说,bugzilla.kernel.org是Linux内核开发者们用来跟踪内核Bug的网站,其中,内核源码主目录中的:ref:admin-guide/reporting-bugs.rst 文件里有一个很好的模板。它指导用户如何报告可能的内核bug以及需要提供哪些信息来帮助内核开发者们找到问题的根源。

对于修复bug的补丁,应该被恰当地介绍、讨论,并拆成独立的小段。小的补丁不需要太多的时间和精力去验证其正确性,同时也会是调试变得非常容易。

当修复补丁时,需要完全地描述补丁:(1)为什么需要这个修改;(2)补丁的总体设计;(3)实现细节;(4)测试结果。

项目详细方案

将page table check补丁合入openEuler5.10

如图,这里是page table check补丁包的所有改动:

page-table-overview.webp

具体的,分成了四个部分,可以在归档中查看每个修改的详细信息:

  1. mm: change page type prior to adding page table entry

    在这个修改中,主要修改了添加页表项的时机,在添加页表项之前修改页面类型。主要涉及到改动的文件有:

    1
    2
    3
    4
    5
    mm/hugetlb.c  | 6 +++---
    mm/memory.c | 9 +++++----
    mm/migrate.c | 5 ++---
    mm/swapfile.c | 4 ++--
    4 files changed, 12 insertions(+), 12 deletions(-)

    在该项目中,可以跟踪这些修改,来对应将它们同步到openEuler5.10中。

  2. mm: ptep_clear() page table helper

    在该修改中,添加了一个新函数——ptep_clear(),能够在通用的代码下从页表中删除PTE项。而且后续将利用这个函数与页表检查建立一个联系(hook)。

    其中,主要涉及到改动的文件有:

    1
    2
    3
    4
    5
    Documentation/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上即可。

  3. 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
    14
    Documentation/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

    跟踪修改,同步支持!

  4. x86: mm: add x86_64 support for page table check

    这个修改中,就是完成之前所说的hook了,即将页面表格检查挂钩添加到修改用户页表的例程中。

    由于在第二个修改中已经完成了相关功能,所以这里的改动并不多,包括:

    1
    2
    3
    arch/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上支持此功能。该补丁集的全部改动如下图所示:

page-table-arm64.webp

包括两个开发者的一共6次修改。

总共的改动其实也不多,可以到归档的文章列表中看到具体每一项的改动信息

page-table-arm64-patch.webp

同3.1的方法,仔细跟踪补丁集所做的修改,做到细心。

整理相关bugfix合入到openEuler5.10

其实,页表检查这个补丁对应的问题可以追溯到2017年的Linux4.14内核中,发现引用数据下溢会导致内存从一个进程泄露到另一个进程。为解决此类内存缺陷,才引入了page table check。

在邮件列表中已经提到出现过的几个bug了,包括:

1654089544902.png

可以将这些作为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相关文档,并合入内核
  • 对项目进行总结思考

Add page table check for openEuler-22.09 · Pull Request !114 · openEuler/kernel - Gitee.com

参考

  1. Google提议用“页表检查”功能应对Linux内核的内存崩坏问题
  2. [[PATCH 5.15 18/20]perf/core: Avoid put_page() when GUP fails - Greg Kroah-Hartman](