LWN:BPF 中可以长存的指针!

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

Long-lived kernel pointers in BPF

July 14, 2022
This article was contributed by David Vernet
DeepL assisted translation
https://lwn.net/Articles/900749/

BPF 子系统允许程序员编写一些可以在内核空间安全地运行的程序。BPF 程序中的所有内存访问和函数调用都是通过内核内的 verifier 进行过静态安全检查的,verifier 在程序允许被加载之前会对其进行整体分析。虽然这样一来来就可以让内核安全地运行 BPF 程序了,但它严重地限制了这些程序能做的事情。在这些限制中,有一条规则是程序不能将指针存储到 BPF mapping 中,以便在今后使用(例如用来进行 dereference 引用或在 kfunc 和 BPF helper 调用中把它们传递给内核)。Kumar Kartikeya Dwivedi 提供的一组 patch 为 BPF 增加了这一功能。

Interacting with kernel pointers in BPF programs

有些操作在 BPF 里运行一定是安全的。例如,bpf_probe_read_user()和 bpf_probe_read_kernel() helper 就允许 BPF 程序通过注册一个 page fault 处理程序来捕捉 faulting address 地址,从而安全地对用户和内核内存分别进行读取。例如,当 BPF 程序从 event handler 程序中收到指针时,它们可以安全地引用访问,而不必担心是否会导致 crash,尽管它们必须通过检查返回的错误代码来确保读取的成功。

BPF 程序也可以从 BPF helper 和 kfuncs 来收到指针。BPF helper 是定义在内核中的函数,提供了可被任何 BPF 程序调用的一组核心 API。Kfuncs 也是内核中可以被 BPF 程序调用的函数,但是与 BPF helper 不同,它们的 API 不要求适用于所有类型的 BPF 程序。为了使一个函数作为 kfunc 来供 BPF 程序使用,它必须被聚合到一个或多个 BPF Type Format(BTF)kfunc-set 中,然后在 BPF 子系统中注册。

Kfunc-sets 也可以给其 kfuncs 指定一些属性,这些属性告知 verifier 需要如何调用从而来确保在 BPF 程序中可以安全执行。其中一个属性就规定,kfunc-set 中的 kfuncs 将返回一个 "acquired" 指针,该指针必须传递给另一个同样属于这个 kfunc-set 的一个 kfunc,从而在那里会被释放。受此限制的指针在 BPF 社区中被称为 "referenced pointers"。

在加载 BPF 程序时,verifier 将履行这个协议,并拒绝任何在返回前未能释放被引用的指针的程序,或者将以前未被返回的指针作为被引用的指针来传递给 BPF helper 或 kfunc 的行为。请注意,kfunc 的 "acquire"和 "release" 语义的实现对 BPF 来说是完全不清楚细节的,完全由实现 kfunc 的开发者来决定。BPF 唯一需要保证的是,确保一个被 acquired 的指针在被 release 之前保持有效。

Extending BPF usability with referenced pointers

确保内核指针有效,就能给 BPF 程序带来几个好处。第一个,也许是最直接的好处,就是 BPF 程序不再需要使用 probed read 来对指针引用进行解析。probed read 使用了 exception table 机制,用于从内核空间安全地读取用户内存,虽然它们在成功读取时具有与正常的 load instruction 几乎相同的性能,但它们要求开发者始终检查这个 read 是否成功,从而给程序员带来了麻烦。避免 probed read 就可以实现更简单的编程模型,可以大大减少 BPF 程序中满足 verifier 所需的相关代码的数量。

除了能让实现更简化之外,引用指针还通过允许 BPF 程序在随后的 kfunc 和 BPF helper 函数调用中安全地将这些指针传回给内核来提高 BPF 的扩展性。虽然内核可以使用诸如 copy_from_user()这样的机制来读取从 BPF 程序收到的指针,但是如果能向内核提供一种保证,证明从 BPF 程序收到的指针是可以安全读取的,这样就不那么复杂了,也不容易出错。这种保证也使得许多内核内部函数在不修改它们的情况下也可以 export 给 BPF 程序了。

虽然引用指针是增强 BPF 对内核的帮助的一个强大工具,但该特性的一个重要限制就是要求 BPF 程序和 kfuncs 之间的所有交互都在同步上下文(synchronous context)中进行。每次需要从内核获得一个指针来引用时,BPF 程序必须调用一个 kfunc,然后在返回之前在另一个 kfunc 调用中释放该指针。这可能会对性能产生影响,因为相对于执行一次内存读取,调用两个 kfuncs 的开销就是相当大了。这种工作流程也与传统的引用计数机制有些重叠,在这种情况下,指针被存储在数据结构中,目的是为了在后续时刻可以安全地访问。取而代之的是将内核指针存储在一个 mapping 中,允许它们在程序需要的时候可以被访问,可能会有多个独立的调用都能来访问。

kptrs - storing kernel pointers in BPF maps

Dwivedi 的 patch set 通过一个名为 "kptrs" 的新特性来增加了这种功能。kptrs 是一个强类型的指针,它是从 kfunc 或 BPF 辅助函数中接收到的,并且可以在程序的整个运行过程中被存储到 BPF mapping 中并被检索到。Kptrs 可以是普通的("未引用的")指针,也可以是引用了的指针。未引用的 kptrs 没有有效性的保证,并且在如何使用方面会受到很大的限制;就像 BPF 程序中的普通指针一样,它们只能通过 probed read 来访问。也不能通过 kfunc 或 BPF helper 函数来传递给内核,因为指针可能会引用无效的内存。另一方面,引用了的 kptrs 可以安全地被 BPF 程序安全地解析和引用,并通过 kfunc 或 BPF helper 函数调用来传递给内核。

从 BPF 子系统的角度来看,一个被引用的 kptr 总是有一个与之相关的引用。为了在不同的上下文之间传递一个 referenced kptr,新开发了一个 bpf_kptr_xchg() helper 函数,它会确保这个 reference 的所有权在 map value 和 local pointer 之间原子地进行交换。如果 reference 从 map value 传递到了本地指针,verifier 就会确保执行的语义与同步上下文中 kfuncs 返回的引用相同:验证器将确保 reference 被转移回 map value,或在当前执行上下文返回之前通过调用 kfunc 来释放。另一方面,如果 kptr 引用被存储在了一个 map 中,那么当前执行上下文可以安全地返回而不用释放。

如果 kptr 从未被 bpf_kptr_xchg() 转出 map 以及手动释放,那么当程序被 unload 以及 map 被销毁时就会自动释放。为了启用这种自动释放机制,Dwivedi 扩展了 kfunc 子系统,允许开发者指定一个应该用于相应类型的 kfunc destructor 函数。

patch set 的早期版本中讨论过 kptrs 的一些使用场景,Dwivedi 描述的最常见的用例是为了性能:

常见的用例是缓存一些对 BPF 内部 map 里的 object 的引用,从而避免昂贵的查找开销,并且在将其传递给多个 helper 时,能够在程序调用的过程中只 raise 一次(以避免后续的重新查找)。

在 map 中存储 referenced kptr,就不需要在每次需要指针的时候调用 kfunc 了,这对性能有好处并降低了复杂性。BPF 程序只需要先调用一个 kfunc 来获取指针,并且在把它存储在一个 map 中后,可以直接从该 map 中加载它并在需要时直接 dereference。

referenced kptrs 也为开发者提供了强大的安全性和正确性的保证。在 managed-object frameworks 框架中,一个 reference 应该只被一个上下文所拥有,这是一个无处不在的原则,kptr reference 的处理语义与 Rust 中的 std::boxed::Box 以及 C++ 中的 std::unique_ptr 在这方面有着惊人的相似之处。在 Rust 中,Box 是一个指向 heap 分配的指针,在任何时候都只能只有一个 reference,当这个 reference 超出范围时,就会被自动释放。每次在编译 Rust 程序时,Rust 编译器利用 Rust 的所有权模型来验证 Box 只能存在一个引用。如果 Rust 程序编译成功了,你就可以确切地知道 Box 所指向的内存是始终有效的,并且只有一个引用。在 C++中有一些验证是在编译时进行的,例如,禁止 std::unique_ptr 被复制,但是在运行时仍有可能出现问题。例如,用户可以两次调用 std::unique_ptr::release(),并在第二次调用时收到一个 nullptr。

Kptrs 似乎从这两种语言中汲取了灵感。一方面,BPF verifier 提供了 Rust 编译器所提供的编译时保证,对 BPF 程序进行分析来确保引用永远只有一个所有者,并且引用永远不会被泄露出去。另一方面,bpf_kptr_xchg()与 std::unique_ptr::swap() 的语义密切相关,所以使用该特性的机制会感觉更像 C++。当要求个人开发者自己来负责审核时,经常会出现一些常见的错误,也就是被管理对象出现泄露,或者 use-after-free 的问题。因此,使用 BPF 和 verifier 为 C 和内核开发提供 Rust 的所有权模型的保证以及 C++的 std::unique_ptr APIs 的语义,似乎确实很强大。

很期待看到后续这些特性所带来的优势是否会促使更多的开发在 BPF 中进行,而不再是在内核中进行。

Future kptr types

虽然我们一直把 unreferenced kptrs 和 referenced kptrs 都称为 "kptr",但它们在 BTF 中实际上是表示为两种不同的类型的。如果 BPF 程序希望将内核指针用作 unreferenced 或 referenced 指针,那么就必须分别用 BTF 类型标签 "kptr "或 "kptr_ref "来进行标注。为每个 kptr 变种都确保一个单独的类型是有其好处的,因为 verifier 需要用 BTF 来知道一个 kptr 的类型,然后相应地确保其可以安全使用。

虽然当前 kptrs 的实现只启用了 unreferenced 和 referenced 这两种变体,这里会自然而然地引出一个问题,那就是该实现是否可以扩展到也包括其他类型的指针。在 patch set 的第一个版本中,Dwivedi 提议为 per-CPU 和用户空间的 kptrs 都增加相应的变种。Alexei Starovoitov 回应问,在 map 中存储 per-CPU 指针的目的是什么?但 Dwivedi 并没有想到具体的用例。我们决定放弃这个功能,直到出现一个更具体的用例再说,所以现在我们必须等待看看是否会有这种需求。

这个 patch set 目前在 linux-next 中,所以大概会在下一个开发周期中被合并。同时,很期待能看到 kfuncs 和 kptrs 未来会如何被用来扩展内核,这将是很有趣的,因为目前 BPF 还无法做到这些。

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

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

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

format,png

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值