LWN:避免对持久性内存的误写入!

关注了就能看到更多这么棒的文章哦~

Stray-write protection for persistent memory

By Jonathan Corbet
February 3, 2022
DeepL assisted translation
https://lwn.net/Articles/883352/

持久性内存(persistent memory)有很多优点,速度快、可以供 CPU 寻址(CPU-addressable)、可用数量大,当然还有持久性。但是,其实它也带来了更高的由于内核中其他 bug 错误而导致这里数据损坏的风险。Ira Weiny 的这组 patch set 就是希望能避免这种问题,它利用了英特尔的 "保护密钥监督器"(PKS, protection keys supervisor)功能,使内核更难以在无意中写入持久性内存。

The stray-write problem

通常,存储在机械硬盘或固态硬盘上的数据只有在 CPU 发出明确请求的情况下才能被覆盖写入,这类复杂的操作会需要好几个步骤才能完成。内核 bug 当然也可能会导致把错误的数据写入驱动器,或者正确的数据最终出现在了错误的地方。所以内核开发人员会竭力防止这种情况发生。另一方面,当内核的目标在做将一个进程移到一个新的 cgroup 或分配一块内存区域这类操作时,不太可能会意外地触发对存储设备的错误写入。确实总是会有 bug 的,但如果一个 bug 恰好完成了触发 I/O 操作的全部所有步骤,这种情况几乎是不可能发生的。

persistent memory 改变了这种情况。一个系统中可以配备几个 TB 字节数量的 persistent memory,其中任何位置都可能包含重要数据,而且所有这些地址也全部都可以由 CPU 直接寻址到。现在,要破坏这些内存的话,所需要的仅仅只是对一个错误指针进行一次写入操作了,这种错误的发生可能性就大多了。被破坏的数据可能会潜藏很多年而不被发现,例如,一些可怜的用户也许发现他们的加密货币钱包已经无法访问了。这种数据损坏似乎很有可能发生,值得努力去防止,无论人们对加密货币的看法是否正面。

要防止这种错误,一个明显方法就是设置针对 persistent memory 的保护措施,以防止内核写入。这些保护措施只有在内核实际需要写到持久性内存时才会被放开,而且只在操作的持续时间内允许改变。存储在页表中的那些常用权限 bit 可以完成这个任务,但要付出巨大性能代价,因为改变页表权限开销是很大的,对每一个持久性内存的写入都要承担这个开销的话,会使大部分的性能提升都消失了,而这些性能提升正是人们使用持久性内存的首要原因。

不过,还有一种方法,至少在英特尔 CPU 上是这样。内存保护键(memory protection keys)功能可以把一个四 bit 的键值跟每个页表条目相关联起来,每个线程都有一个 mask,用来设置当前针对每个键值的读写权限。例如,该掩码可能会要求所有标有键值 3 的页面都是可读的,但当前线程不可对其写入。改变掩码并不是一个特权操作,所以这个功能不是一个严格的安全功能,但它们可以提高恶意访问的门槛,防止意外的发生。同时,改变访问掩码的操作非常快速,可以一下子改变大量 page 的可访问权限,所以它是一个远比调整 page 权限更有效的可以用来提供这种保护的方式。

Protecting persistent memory with PKS

内存保护键值可以用在任何类型的内存上。例如有一个使用场景就是保护普通内存中的加密密钥。从 2016 年的 4.9 版本开始,Linux 就支持用户空间的内存保护密钥(memory protection keys for userspace)。但内存保护密钥也可以用在内核空间,每个页面都有一个特定的内核键值,内核的内存保护键值也被称为 PKS。不过 Linux 目前还没有利用这个功能。因此,Weiny 的 44 个 patch 的 patch set 的第一个目标就是在内核中增加对 PKS 的支持,这个任务涉及到许多小细节。

例如,PKS 的 access-control map 被存储在一个 MSR 寄存器(model-specific register)中,这使得改动操作会相对快速。不过,当一个线程被抢占时,CPU 并不会自动保存该 MSR 的值,所以内核必须自己处理这个细节。这涉及到对 pt_regs 结构体的一些细节改动,从而让它可以保存额外的信息而不破坏那些之前并不知道会加这些内容的代码。新增的 "auxiliary pt_regs "结构可以保存 PKS permission mask 掩码信息,以及其他一些将来肯定需要保存的信息。

还需要一种方法来分配内核中的 16 个可用键值。其中,0号键值,也就是应用于所有 page 的默认键值,必须保留 "允许所有访问" 的权限,因此还有 15 个键值可以用于其他用途。在目前的实现中,这些键是在代码中静态分配的。如果内核的密钥用完了,这种方法就必须改变,但目前还不清楚是否有那么多用户会真的全部用光 16 个。在出现这种情况之前去增加一个更复杂的密钥分配机制,似乎没有多大价值,所以静态分配的方法就占了上风。在这组 patch 中,键值 1 被保留给 kernel self tests,键值 2 用于保护 stray-write。

有了这个基础架构之后,后面的事情就很简单了,可以用新的键值来配置持久性内存 page,并将访问权限设置为拒绝写入。这就可以防止任何 stray-write 操作来破坏内存内容了,这是好事,但同时也会阻止合法的写操作,这就不太妙了。因此,最后一步是在内核需要对持久性内存 page 进行写入时改变权限。这在直接处理持久性内存的驱动代码中很容易实现。只要在在写之前和写之后加上正确的 API 调用就好。再提醒一下,对 permission mask 的改动只适用于当前线程,所以当这个操作正在进行时,内核的其他部分无法写入到持久性内存。

不过还有一个问题,就是内核更高层级的一些代码可能也需要对持久性内存进行写入。一个例子是文件系统中的 DAX 代码,它是用来直接向持久性内存读写数据、而不需要通过 page cache。试图在每一个可能写入持久性内存的位置的周围都添加相关的调用,这种做法通常注定会失败,所以需要另一种方法。

碰巧,内核代码已经被要求在访问任意的内存页之前都要进行特定调用。这些调用是以 kmap() 系列函数的形式,目前在大多数 64 位系统上并没有任何作用,但是在 32 位系统上都是需要调用的,因为这些系统上无法直接将所有的物理内存全部映射到内核的地址空间。Weiny 的 patch set 会让短期的调用(如 kmap_atomic()和 kmap_local_page())来检查相关内存是否受到持久性内存键值的保护。如果受到保护的话,权限就会被修改,从而允许在 map 生效期间对持久性内存进行写入。受保护的持久性内存不允许使用长期生效的映射(也就是用普通的 kmap() 来映射),所以对持久性内存进行写入的时间窗口会被保持得很小,并且只限于确实需要写入的线程。

这个 patch set 已经是第八次修订了,自从 2020 年中在这里介绍第二版本以来,它已经发生了很大的变化。一些核心开发人员已经仔细看过并提出了意见,所有大部分功能被重新设计过了。这个版本是否能通过 review 还有待观察,但应该是越来越接近完成了。目标似乎是非常正当的,CPU 提供了一个可以有效检测和预防事故的机制,内核应该要利用它。

全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。

欢迎分享、转载及基于现有协议再创作~

长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~

270218f66675bd0577f485875f5d7f18.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值